Latest Posts (5 found)
Florian Zenker 1 months ago

Infinite-Dimensional Vectors

Vector spaces are incredibly useful and, consequently, ubiquitous in modern mathematics. That’s not a coincidence; the modern definition of vector spaces is about 100 years old and the result of about 300 years of evolution 1 . Vector spaces are in fact so useful that physics, engineering, computer science and many other disciplines make use of them on a daily basis to model a large range of problems; from modeling the world in theoretical physics to representing meaning and concepts in machine learning. If you work in software, engineering, physics, or machine learning, you’ve almost certainly been trained to think of vectors as lists of numbers or arrows. In modern mathematics, that’s not what a vector is, and that intuition collapses the moment you leave finite dimensions. I hope this article will change how you think about vectors and dimensions by breaking down assumptions about vector spaces. Unfortunately, finite-dimensional vector spaces are not a good basis to build an intuition for infinite-dimensional vector spaces. So let’s forget about ℝ n and start from scratch. I am going to try to beeline into infinite dimensions and ignore a number of important topics about vector spaces. Before we can talk about dimensions at all, we must first stop seeing vectors as lists of numbers or geometric arrows. To do that, we need to start with the mathematical definition. Definition: Let V be a non-empty set and 𝔽 be a field (e.g. ℚ , ℝ , or ℂ ). V is a vector space over the field 𝔽 if there are two functions fulfilling the following axioms for all u , v , w ∈ V and α , β ∈ 𝔽 : It’s common to use regular addition ( a + b ) and multiplication ( a b ) notation to mean vector addition or scalar multiplication based on context. That’s perfectly fine when these operations are not the focus. However, when introducing vector spaces it pays off to be explicit. So for this section, we’re going to use different symbols to introduce the concept and we’ll switch to the regular syntax in the following sections. What vector spaces are not It’s important to note that there are a number of things that are reasonable to expect from vector spaces that are not part of the definition of vector spaces. This means that the very common intuition for vectors of having magnitude and direction doesn’t actually apply to general vectors. There’s a class of vector spaces called Hilbert spaces for which this intuition works, but they are out of scope for this article. Let’s try to understand what that means by looking at a practical example. My favorite kinds of examples have surprises that challenge preexisting intuitions, so I recommend paying special attention to them. Example: Let V be the set of positive real numbers V = { x ∈ ℝ | x > 0 } . If we define vector addition and scalar multiplication with ℝ as Then V is a vector space over ℝ : Notice that vector addition and scalar multiplication are not constructed from field addition and multiplication respectively. This is important; vector spaces don’t have to follow the same construction as the standard vector spaces ℝ n . One thing that’s interesting about this vector space is that every vector v can be represented as v = λ ⊙ e = e λ with λ = ln v . We’ll see later that this means that this vector space has the dimension 1 . No more weird symbols We’re now going to switch to a less noisy syntax. Instead of ( α ⊙ v ) ⊕ ( β ⊙ u ) we’ll just write α v + β u . However, don’t let that fool you, vector addition and scalar multiplication may behave similarly to their standard counterparts, but they are very different. Bases are the most important concept to understand vector space dimensions. They are a central concept in vector space theory in general, so it’s well worth having a basic understanding of them. In short, a basis is a minimal set of vectors that spans the whole vector space. If we have any set of vectors S ⊆ V of a vector space V over 𝔽 , the span of that set is the set of all finite linear combinations of elements of S : where n = 0 gives the empty sum 0 V , so span ( ∅ ) = { 0 V } . For S to be a basis of V , S needs to be minimal and span ( S ) = V . If S is not minimal, we can remove an element, say v r ∈ S , such that S ∖ { v r } ( S without v r ) still spans V . This means there’s a linear combination without v r that can represent v r , after all, v r is an element of V = span ( S ∖ { v r } ) : This works the other way around as well: If S contains a vector v r that can be represented with a linear combination not involving v r , we can remove this vector and still span the same vector space. This gives us a better way to express minimality: We call S linearly independent if removing any element from S strictly shrinks its span: span ( S ∖ { v r } ) ⊊ span ( S ) or, equivalently, no vector in S can be expressed as a linear combination of other vectors in S . Definition: A basis B of a vector space V over a field 𝔽 is a linearly independent subset of V that spans V . The elements of B are called basis vectors . A consequence of linear independence is that every vector in V has a unique representation as a linear combination of basis elements. You might be wondering why we’re only interested in finite linear combinations and exclude infinite linear combinations. The reason is that infinite linear combinations require a concept of convergence which we don’t have for general vector spaces and even if we did have it, not every infinite linear combination would converge. When we add convergence to a vector space (like in Hilbert or Banach spaces), we can talk about Schauder bases with infinite series. But in general vector spaces, we are strictly talking about Hamel bases (i.e. finite linear combinations). The reason why bases are so important is that every vector space has a basis and that all bases of the same vector space have the same cardinality 2 . This lets us define the dimension of a vector space: Definition: Let B be a basis for the vector space V , then the dimension of V is dim ( V ) = | B | . This definition matches our intuition for finite-dimensional vector spaces very well, and it even works for a number of infinite-dimensional vector spaces too. Example: ℝ [ x ] is the vector space of polynomials The set of monomials { 1 , x , x 2 , x 3 , … } is a basis of this vector space. It has countably many elements, so the dimension of ℝ [ x ] is countably infinite. However, when it comes to infinite-dimensional vector spaces we quickly run into cases where our intuition stops working. Example: C ∞ ( [ 0 , 1 ] ) is the set of all real-valued functions that are continuous and infinitely differentiable on the interval [ 0 , 1 ] ⊂ ℝ . C ∞ ( [ 0 , 1 ] ) forms an infinite-dimensional vector space over ℝ using the usual function addition and scalar multiplication operations: The functions in C ∞ ( [ 0 , 1 ] ) are about as well-behaved as functions get and yet there is no countable basis for this vector space: The set { x ↦ e λ x | λ ∈ ℝ } is linearly independent and uncountable, so the dimension of C ∞ ( [ 0 , 1 ] ) is at least | ℝ | . While countable sets of functions like polynomials can approximate smooth functions arbitrarily well, pure linear combinations only allow finite sums. Any finite combination of polynomials is just another polynomial of finite degree; it can never produce e x or sin ( x ) . But we can construct even simpler vector spaces with uncountably many dimensions. Example: ℝ is an infinite-dimensional vector space over ℚ with vector addition and scalar multiplication defined using standard addition and multiplication. The set of vectors is the real numbers and the field over which this is a vector space is the rational numbers. Linear combinations are of the form That is, the coefficients are rational numbers and the vectors are real numbers. Let’s assume that the dimension of this vector space is finite. Then this vector space has a basis B = { b 1 , b 2 , … , b n } ⊂ ℝ and every element in ℝ has a representation as a linear combination of this basis: However, this would imply | ℝ | = | ℚ n | , but that is a contradiction: Since ℚ is countable, the product ℚ n is also countable. However, ℝ is uncountable, so | ℝ | ≠ | ℚ n | . Therefore, the dimension of this vector space cannot be finite. In fact, because any finite linear combination over a countable basis would still only generate a countable set of numbers, the dimension must be uncountably infinite. This leads us directly to one central question that is very confusing: What does a basis of an infinite-dimensional vector space actually look like? Unfortunately, the answer to that is that when infinity rears its beautifully enormous head, constructive mathematics tends to leave the room. The proof that all vector spaces have a basis requires the Axiom of Choice . It states that for any set of non-empty sets, there exists a choice function that maps each of the element sets to an element of that set. Whenever the Axiom of Choice is needed, it means we don’t know what that choice function is, which leaves us with no example of the object whose existence was proven. In fact, if the Axiom of Choice is not assumed, but instead we assume that every vector space has a basis as an axiom, then it’s possible to prove the Axiom of Choice. That is, both are equivalent 3 . To see why our intuition fails so severely for these infinite-dimensional vector spaces, it helps to revisit why finite dimensions feel so intuitive in the first place: coordinates . One closely related topic to vector space bases is vector space coordinates. If we have a finite basis B = ( b 1 , b 2 , … , b n ) of a vector space V over 𝔽 , we can define a map ϕ : 𝔽 n → V with This allows us to represent any finite-dimensional vector space in the usual way using coordinate tuples. In fact, this allows us to handle any finite-dimensional vector space over ℝ like the standard vector spaces ℝ n . Example: Let B = ( x ↦ sin ( x ) , x ↦ cos ( x ) ) be the basis of a vector space of functions V ⊂ C ∞ ( ℝ ) . By construction, this vector space has the dimension dim ( V ) = 2 . With this basis, we can define a coordinate system ϕ : ℝ 2 → V with We can handle this vector space the same way as the well-known ℝ 2 vector space. This is actually very useful in practice, for example it can be used to visualize Quadrature Amplitude Modulation (QAM) using Constellation Diagrams . Finite-dimensional function spaces like this also come up in solutions to homogeneous linear differential equations. In this case, the vector space V is the space of all solutions to f ″ + f = 0 . Thinking of vector spaces as standard ℝ n coordinate tuples is natural because coordinates make finite-dimensional spaces easy to navigate. But this convenient representation is not what defines vector spaces. When a vector space has an infinite basis, the coordinate representation breaks down and our intuition for vector spaces stops working. We need to build a different kind of intuition. I titled this article Infinite-Dimensional Vectors , but there is no such thing: Dimensionality is not a property of a vector, but of the space it’s defined in. Consequently, a single vector from an infinite-dimensional vector space is something a human can comprehend. Furthermore, every vector in a vector space consists of a finite linear combination of basis vectors, even in infinite dimensions. This means that every calculation on vectors is contained in a finite-dimensional subspace. Consider an infinite-dimensional vector space V and two vectors v , w ∈ V . Because v and w are made of a finite number of basis vectors, adding them is an operation confined to a finite-dimensional subspace. More generally, any finite set of vectors can only span a finite-dimensional subspace and every theorem from standard linear algebra applies there. In other words, even infinite-dimensional vector spaces are structures we can reason about. We can even use our existing finite-dimensional intuition locally. However, there’s a catch: Bases for infinite-dimensional vector spaces can be impossible to grasp. To make infinite-dimensional vector spaces truly useful, that problem needs to be addressed. This is what topology was invented for, but that’s a topic for a different article. ⊕ : V × V → V (vector addition) ⊙ : 𝔽 × V → V (scalar multiplication) u ⊕ ( v ⊕ w ) = ( u ⊕ v ) ⊕ w (associativity) v ⊕ u = u ⊕ v (commutativity) There exists a 0 V ∈ V so that for all v ∈ V : v ⊕ 0 V = 0 V ⊕ v = v (neutral element) For every v ∈ V , there exists an inverse − v ∈ V with v ⊕ ( − v ) = ( − v ) ⊕ v = 0 V (inverse element) α ⊙ ( u ⊕ v ) = ( α ⊙ u ) ⊕ ( α ⊙ v ) (distributivity of scalar multiplication over vector addition) ( α + β ) ⊙ v = ( α ⊙ v ) ⊕ ( β ⊙ v ) (distributivity of scalar multiplication over field addition) ( α ⋅ β ) ⊙ v = α ⊙ ( β ⊙ v ) (compatibility of scalar multiplication with field multiplication) 1 𝔽 ⊙ v = v (scalar identity) . We don’t have the concept of a distance between two vectors (i.e. we don’t have a metric). We don’t have the concept of the magnitude for a vector (i.e. we don’t have a norm). We don’t have the concept of a direction of a vector (i.e. we don’t have an inner product). We don’t have any general concept of convergence within a vector space. ⊕ : V × V → V defined as ( u , v ) ↦ u ⋅ v with the neutral element 0 V = 1 ℝ and the inverse element − v = 1 v ⊙ : ℝ × V → V defined as ( α , v ) ↦ v α u ⊕ ( v ⊕ w ) = u ⋅ ( v ⋅ w ) = ( u ⋅ v ) ⋅ w = ( u ⊕ v ) ⊕ w v ⊕ u = v ⋅ u = u ⋅ v = u ⊕ v v ⊕ 0 V = v ⋅ 1 ℝ = 1 ℝ ⋅ v = 0 V ⊕ v = v v ⊕ ( − v ) = v ⋅ 1 v = 1 v ⋅ v = ( − v ) ⊕ v = 1 ℝ = 0 V α ⊙ ( u ⊕ v ) = ( u ⋅ v ) α = u α ⋅ v α = ( α ⊙ u ) ⊕ ( α ⊙ v ) ( α + β ) ⊙ v = v α + β = v α ⋅ v β = ( α ⊙ v ) ⊕ ( β ⊙ v ) ( α ⋅ β ) ⊙ v = v α ⋅ β = ( v β ) α = α ⊙ ( β ⊙ v ) 1 ℝ ⊙ v = v 1 ℝ = v . ( f + g ) ( x ) = f ( x ) + g ( x ) ( α f ) ( x ) = α f ( x ) A general outline of the genesis of vector space theory ↩ This is the basis theorem . ↩ Existence of bases implies the axiom of choice ↩

0 views
Florian Zenker 1 years ago

Diff Algorithms

However, heuristics like this trade diff minimality for performance, this is not always desirable. Sometimes, a minimal diff is exactly what's required. disables these heuristics to always find a minimal diff irrespective of the costs. We established before that a diff algorithm finds one of many possible solutions. Given such a solution we can discover more solutions by it locally and then selecting the best solution according to some metric. This is exactly how Michael Haggerty's indentation heuristic works for text. For any given diff, we can often slide the edits up or down in a way that doesn't change the meaning of a diff. For example, has the same meaning as We call edits that can be slid up or down sliders . The question is, how do we select the best slide? Michael collected human ratings for different sliders of the same diff and used them to develop a heuristic to match these ratings: diff-slider-tools . However, this heuristic only works for text and is tuned towards code instead of prose. I decided to make it optional. It can be enabled with the option. The representation used during the execution of the diff algorithm has a surprising impact on the algorithm performance and result readability. This is not at all obvious, and so it took me a while to figure out that the best approach is akin to a side-by-side view of a diff: You use two slices to represent the left side and the right side respectively: in the left side slice represents a deletion and on the right side an insertion. is a matching element. This representation has four big advantages: It can be preallocated, the order in which edits are discovered doesn't matter, it's easy to mutate during post-processing, and it's easy to generate other representations from it. Diff algorithms are relatively complicated by themselves, but they pale in comparison to what's necessary to provide a high-quality diff library. This article tries to explain what went into my new diff library, but there's still more that I haven't implemented yet. Here is one real-world example of why worst-case scenarios are important: Imagine you're breaking an existing feature in a way that triggers a worst-case scenario in a test. If the test is running for a very long time or runs out of memory, you're going to have to debug two problems instead of one.  ↩︎   ↩︎ See benchmark_comparison.txt for the source of these ratings.  ↩︎   ↩︎   ↩︎   ↩︎ No support for arbitrary sequences  ↩︎   ↩︎   ↩︎   ↩︎ No support for unified diffs  ↩︎   ↩︎ The diffmatchpatch API is very hard to use  ↩︎ No support for structured results  ↩︎ Quadratic memory use; for my test cases, this resulted in >30 GB of memory used.  ↩︎ The mb0 API is from before generics and is a bit cumbersome to use  ↩︎ udiff has a very low threshold for when it starts to stop searching for an optimal solution. This improves the speed, but it also results in relatively large diffs.  ↩︎   ↩︎ There's no single patience diff heuristic, instead there are different implementations with different performance characteristics.  ↩︎ Stolen from https://blog.jcoglan.com/2017/03/22/myers-diff-in-linear-space-theory/   ↩︎ I can recommend https://blog.robertelder.org/diff-algorithm/ and this 5 part series https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/   ↩︎ Myers, E.W. An O(ND) difference algorithm and its variations. Algorithmica 1, 251-266 (1986). https://doi.org/10.1007/BF01840446   ↩︎

0 views
Florian Zenker 1 years ago

Using Automated Refactoring Tools in Anger

Go supports functions as a first class concept and it's pretty common to pass a function to inject custom behavior. This can have a performance impact. In the ideal case, like in the example below, the compiler can inline everything, resulting in optimal performance: The loop in is inlined into , as is the function we're passing in as a parameter. The resulting code in the loop is free of function calls (no instruction): However, in many cases, where the function that's called can't be inlined because it's too complex (or if we disallow the compiler to inline) Go needs to make an indirect function call from within the loop. Let's use to forbid the compiler from inlining and compare the generated assembly code: In this case, the loop is not inlined into and we clearly see an indirect function call. The call instruction is the function call, and it's indirect because it's using a register ( ) as an argument instead of a label. In many cases, the difference between the two is unnoticeable. It would definitely not be the right decision to always inline this function, and if the function is truly hot, Go may still inline it when using profile guided optimization . I ran into this while investigating optimization opportunities for znkr.io/diff . I had started out with a generic implementation of the diffing algorithm that works for any type, using a signature like: However, the implementation of diffing algorithms is rather complex and so the function was never inlined. After a number of rounds of optimizations, I ended up with a changed API and an implementation that collapsed all diffs to using the algorithm on an type 1 . The problem was that the function still used an underlying algorithm for and supplied an function that was never inlined. My hypothesis was that I could improve the performance by making sure that the function was inlined. However, I didn't know how to do that. I tried to validate the hypothesis with a simple hack that duplicated the implementation and specialized it by hand. To my surprise, that hack resulted in a runtime reduction by up to -40% . So clearly, that's a worthwhile optimization! Unfortunately, the diff implementation is a bit too complicated to be a good example for this blog post. Instead, let's use a very simple (but inefficient) Quick Sort implementation (which incidentally has a similar recursive structure as the diff implementation I am using): Manually specializing this function for is straightforward: Comparing the runtime of these two algorithms using a simple benchmark (sorting slices with 100 random numbers) on my M1 MacBook Pro also shows a runtime improvement of -40%: I tried a number of ideas but found no way to ensure the passed-in function was inlined. The alternative of maintaining two copies of the same algorithm didn't sound very appealing. It felt like I had to either reduce the API surface by removing the option, take a performance hit to have both, or maintain two versions of the same algorithm. I really wished for specialization that would allow me to write the algorithm once and change aspects of it for types. I liked none of these options, and I was despairing about which one to pick when it hit me: I could implement specialization myself! Go has excellent support for refactoring Go code thanks to the go/ast package. We can use that to build a code generator that performs the manual specialization described above automatically: This is overkill for a simple function like , but keep in mind that this is a simple example by construction. The same principle can be applied to much more complicated functions or even, as in the case that triggered me to do this, whole types with multiple functions. It's also fairly trivial to hook the specialization into a unit test to validate that the specialized version matches the output of the generator and use to regenerate the specialized version. The full version of what I ended up using for the diffing algorithm is here . I found a way to specialize functionality in a way that doesn't require manual maintenance. Is it a good idea, though? I don't really know, but I can drop the specialized version for a performance hit with only a few lines of code. If this turns out to be a bad idea, or if I or someone else finds a better solution, it's quite easily removed or replaced. The details don't matter here, but the idea is to assign a unique integer to every line in both inputs. This happens almost naturally when reducing the problem size by removing every line unique to either input. Both together significantly speed up a number of diffs.  ↩︎

0 views
Florian Zenker 2 years ago

Point-to-Point Routing

When I upgraded my NAS to have a 10G network interface I didn't have a 10G switch in my network. To still have a fast 10G connection I installed a direct link between my desk and my TrueNAS Scale box. That link existed in parallel to my regular 1G LAN link: Let's say we use the subnet for the LAN and for the point-to-point link 1 . An easy way to use the 10G link would be to always use my NAS's IP address on the point-to-point subnet when accessing my NAS. That's a little inconvenient though, I would prefer to use the hostname which resolves to . If my computer was stationary and always connected to the two links, I could simply override the hostname (e.g. by editing ). However, my computer is MacBook and I use it from the couch as well as from my desk. When I am at my desk, I use a Thunderbolt dock that's connected to all my periphery and the two network links. In that case, I want NAS connections to use the 10G link. But when I am using it from anywhere else, I want to use my Wifi link or VPN. This is not an impossible setup, IP routing is very powerful and my intuition was that this should be possible to use the 10G link transparently using IP routing. However, I did not find a guide to describe how to set this up, and needed to piece it together. All IP routing works by sending packets intended for one IP address to a different IP address without actually changing the destination IP address in the IP packet. On ethernet links that usually means sending these packets to the MAC address. The receiver of such a packet then decides what to do with that packet. A router will usually forward the packet to a different router until it reaches its destination. The most common case is to just send everything that's not on any local subnet to a default gateway . In home networks that's often the router that connects the LAN to the internet and forwards the packets to the ISPs router. For point-to-point routing, we add a static entry into the routing table on both ends of the point-to-point link that routes all packets intended for the other end to the point-to-point subnet address instead. That is, my MacBook, when connected to the 10G link, should send all IP packets intended for to and my NAS should send all packets intended for to . This is enough because both Linux and MacOS implement the weak host model : They don't care at what interface packets arrive. If a packet for arrives at the interface configured for , both Linux and MacOS will accept it if a different interface is configured for . On TrueNAS adding a static route is straightforward, specifying the destination as and the gateway as is all that's necessary: The destination is CIDR for "only ", so only packets intended for that IP address will be routed to . The setup on MacOS requires some command-line shenanigans though. First, we need to figure out the name of the 10G device. The simplest way I found was to run and compare with system settings: In my case "Thunderbolt Ethernet Slot 0" is the 10G point-to-point link and "USB 10/100/1000 LAN" is the 1G LAN link. Setting a static route on the "Thunderbolt Ethernet Slot 0" device works like this: Here, instead of the CIDR notation used by TrueNAS, the subnet mask makes sure to select only the IP address to route to . Neither of the two systems allows setting the source IP address. That's a bummer because without that connections using the 10G link use the point-to-point IP address instead of the LAN address. However, I haven't run into any issues because of that yet. I know I could be using a here, but they are a bit special and I didn't want to run into any issues. It's unlikely that I'll be using more than 1 point-to-point link ever, so the is already more for documentation than to save IP address resources.  ↩︎

0 views
Florian Zenker 2 years ago

TrueNAS ACME using deSEC.io

TrueNAS can secure connections to its WebUI using ACME-provisioned TLS certificates. Because I don't want to expose TrueNAS to the internet, the best possible way to get a certificate is to use ACME DNS-01. Unfortunately, TrueNAS only supports Cloudflare and AWS Route 53 out of the box. For other DNS providers, a shell script can be used. I am using deSEC.io and so I needed to write a shell script. I struggled quite a bit with this because there's not really any documentation out there about how to do it. Assuming TrueNAS tries to get a certificate for it will call the shell script with the following parameters. To set the challenge, it calls Once the challenge is either solved or failed it removes the challenge from DNS by calling It doesn't attempt to find the zone for you instead it gives you the full domain name (e.g. let's say the zone in this example is , then TrueNAS will provide instead of ). That's annoying because for deSEC I need the zone. I decided to hard code the zone into the script to avoid anything more complicated than calling . It's not the most robust piece of software, but it does its job.

0 views