2024年11月

教程名称:使用 C# 入门深度学习

作者:痴者工良

地址:

https://torch.whuanle.cn

线性代数

推荐书籍

大家都知道学习 Pytorch 或 AI 需要一定的数学基础,当然也不需要太高,只需要掌握一些基础知识和求解方法,常见需要的数学基础有线性代数、微积分、概率论等,由于高等数学课程里面同时包含了线性代数和微积分的知识,因此读者只需要学习高等数学、概率论两门课程即可。数学不用看得太深,这样太花时间了,能理解意思就行。


首先推荐以下两本书,无论是否已经忘记了初高中数学知识,对于数学基础薄弱的读者来说,都可以看。

  • 《普林斯顿微积分读本》

  • 《普林斯顿概率论读本》


国内的书主要是一些教材,学习难度会大一些,不过完整看完可以提升数学水平,例如同济大学出版的《高等数学》上下册、《概率论与数理统计》,不过国内的这些教材主要为了刷题解题、考研考试,可能不太适合读者,而且学习起来的时间也太长了。


接着是推荐《深度学习中的数学》,作者是涌井良幸和涌井贞美,对于入门的读者来说上手难度也大一些,不那么容易看得进去,读者可以在看完本文之后再去阅读这本经典书,相信会更加容易读懂。

另外,千万不要用微信读书这些工具看数学书,排版乱七八糟的,数学公式是各种抠图,数学符号也是用图片拼凑的,再比如公式里面中文英文符号都不分。

建议直接买实体书,容易深度思考,数学要多答题解题才行。就算买来吃灰,放在书架也可以装逼呀。买吧。


本文虽然不要求读者数学基础,但是还是需要知道一些数学符号的,例如求和∑ 、集合交并∩∪等,这些在本文中不会再赘述,读者不理解的时候需要自行搜索资料。


基础知识

标量、向量、矩阵

笔者只能给出大体的概念,至于数学上的具体定义,这里就不展开了。

标量(scalar):只有大小没有方向的数值,例如体重、身高。

向量(vector):既有大小也有方向的数值,可以用行或列来表示。

矩阵(matrix):由多行多列的向量组成。

张量(Tensor):在 Pytorch 中,torch.Tensor 类型数据结构就是张量,结构跟数组或矩阵相似。


  • Tensor:是PyTorch中的基本数据类型,可以理解为多维数组。 Tensor可以用来表示数据集、模型参数和模型输出等。
  • Scalar:是一个特殊类型的Tensor,只有一维。 Scalar用来表示标量值,如学习率、损失值等。
  • Vector:是一个特殊类型的Tensor,有一维或两维。 Vector用来表示向量值,如梯度、特征值等。
  • Matrix:是一个特殊类型的Tensor,有两维。 Matrix用来表示矩阵值,如权重矩阵、输出矩阵等。


比如说 1.0、2 这些都是标量,在各种编程语言中都以基础数据类型提供了支持,例如 C# 的基元类型。


下面将标量转换为 torch.Tensor 类型。

var x = torch.tensor(1.0);
var y = torch.tensor(2);

x.print_csharp();
y.print_csharp();
[], type = Float64, device = cpu, value = 1
[], type = Int32, device = cpu, value = 2


将数组转换为 torch.Tensor 类型:

var data = new int[ , ]{ {1, 2}, { 3, 4}};
var x_data = torch.tensor(data);

x_data.print_csharp();


由于上一章已经讲解了很多数组的创建方式,因此这里不再赘述。


Pytorch 的一些数学函数

Pytorch 通过 torch.Tensor 表示各种数据类型,torch.Tensor 提供超过 100 多种的张量操作,例如算术运算、线性代数、矩阵操作、采样等。

由于篇幅有限,这里就不单独给出,读者请自行参考以下资料:

https://pytorch.org/docs/stable/torch.html

https://pytorch.ac.cn/docs/stable/torch.html


线性代数


向量

向量的概念

在研究力学、物理学等工程应用领域中会碰到两类的量,一类完全由
数值的大小
决定,例如温度、时间、面积、体积、密度、质量等,称为
数量

标量
,另一类的量,
只知道数值的大小还不能完全确定所描述量
,例如加速度、速度等,这些量除了大小还有方向,称为向量。


在平面坐标轴上有两点
\(A(x_{1},y_{1})\)

\(B(x_{2},y_{2})\)
,以 A 为起点 、B 为终点的线段被称为被称为有向线段,其既有大小也有方向,使用 $\overrightarrow{AB} $ 表示,使用坐标表示为
\((x_{2}-x_{1},y_{2}-y_{1})\)
,如果不强调方向,也可以使用 $\alpha $ 等符号进行简记。

image-20241108071154361


A、B 之间的距离称为向量的模,使用 | $\overrightarrow{AB} $ | 或 | $\overrightarrow{BA} $ | 或 | $\alpha $ | 表示。

平面中的向量,其距离公式是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2}
\]


其实原理也很简单,根据勾股定理,AB 的平方等于两个直角边长平方之和,所以:

\[| \overrightarrow{AB} | ^2 = (x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2
\]


image-20241108212312023


去平方就是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2}
\]


如下图所示,其两点间的距离:

\[ | \overrightarrow{AB} | = \sqrt{(4-1)^{2} + (4-1)^2} = \sqrt{18} = 3\sqrt{2} = 4.242640687119285
\]


image-20241108071828663


使用 C# 计算向量的模,结果如下

var A = torch.from_array(new[] { 1.0, 1.0 });
var B = torch.from_array(new[] { 4.0, 4.0 });
var a = B - A;

var norm = torch.norm(a);
norm.print_csharp();
[], type = Float64, device = cpu, value = 4.2426

注意,计算向量的模只能使用浮点型数据,不能使用 int、long 这些整型。


同理,对于三维空间中的两点
\(A(x_{1},y_{1},z_{1})\)

\(B(x_{2},y_{2},z_{2})\)
,距离公式是:

\[| \overrightarrow{AB} | = \sqrt{(x_{2}-x_{1})^{2} + (y_{2}-y_{1})^2 + (z_{2}-z_{1})^2}
\]


向量的加减乘除法

向量的加法很简单,坐标相加即可。

如图所示,平面中有三点 A(1,1)、B(3,5)、C(6,4)。

image-20241108205142069


得到三个向量分别为:$\overrightarrow{AB} (2,4)
\(、\)
\overrightarrow{BC} (3,-1)
\(、\)
\overrightarrow{AC} (5,3) $


根据数学上向量的加法可知,$\overrightarrow{AB} $ + $\overrightarrow{BC} $ = $\overrightarrow{AC} $

var B = torch.from_array(new[] { 2.0, 4.0 });
var A = torch.from_array(new[] { 3.0, -1.0 });
var a = A + B;

a.print_csharp();
[2], type = Float64, device = cpu, value = double [] {5, 3}


同理,在 Pytorch 中,向量减法也是两个 torch.Tensor 类型相减即可。

推广到三维空间,计算方法也是一样的。

var B = torch.from_array(new[] { 2.0, 3.0, 4.0 });
var A = torch.from_array(new[] { 3.0, 4.0, 5.0 });
var a = B - A;

a.print_csharp();
[3], type = Float64, device = cpu, value = double [] {-1, -1, -1}


另外,向量乘以或除以一个标量,直接运算即可,如 $ \overrightarrow{AB} (2,4) $,则 $ 3 * \overrightarrow{AB} (2,4) $ = (6,12)。


向量的投影

如图所示, $\overrightarrow{AB} (2,4) $ 是平面上的向量,如果我们要计算向量在 x、y 上的投影是很简单的,例如在 x 轴上的投影是 2,因为 A 点的 x 坐标是 1,B 点的 x 坐标是 3,所以 3 - 1 = 2 为 $\overrightarrow{AB} (2,4) $ 在 x 轴上的投影,5 - 1 = 4 是在 y 轴上的投影。

image-20241108211302187


在数学上使用
\(Projx(u)\)
表示向量 u 在 x 上的投影,同理
\(Projy(u)\)
是 u 在 y 上的投影。

如果使用三角函数,我们可以这样计算向量在各个轴上的投影。

\[Projx(u) = |\overrightarrow{AB}| \cos \alpha = |\overrightarrow{AC}|
\]

\[Projy(u) = |\overrightarrow{AB}| \sin \alpha = |\overrightarrow{BC}|
\]


AC、BC 长度是 4,根据勾股定理得出 AB 长度是 $4\sqrt{2} $,由于
\(cos \frac{\pi }{2} = \frac{\sqrt{2}} {2}\)
,所以
\(Projx(u) = 4\)

image-20241108212445350


那么在平面中,我们已知向量的坐标,求向量与 x 、y 轴的夹角,可以这样求。

\[\cos \alpha = \frac{x}{|v|}
\]

\[\sin \alpha = \frac{y}{|v|}
\]


例如上图中 $\overrightarrow{AB} (4,4) $,x 和 y 都是 4,其中
\(|v| = 4\sqrt{2}\)
,所以
\(\cos \alpha = \frac{4}{4\sqrt{2}} = \frac{\sqrt{2}}{2}\)


从 x、y 轴推广到平面中任意两个向量
\(\alpha\)

\(\beta\)
,求其夹角
\(\varphi\)
的公式为:

\[\cos \varphi = \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|}
\]


继续按下图所示,计算
\(\overrightarrow{AB}\)

\(\overrightarrow{AC}\)
之间的夹角,很明显,我们按经验直接可以得出夹角
\(\varphi\)
是 45° 。

image-20241108221035111


但是如果我们要通过投影方式计算出来,则根据 $ \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|} $ ,是 C# 计算如下。

var AB = torch.from_array(new[] { 4.0, 4.0 });
var AC = torch.from_array(new[] { 4.0, 0.0 });

// 点积
var dot = torch.dot(AB, AC);

// 求每个向量的模
var ab = torch.norm(AB);
var ac = torch.norm(AC);

// 求出 cosφ 的值
var cos = dot / (ab * ac);
cos.print_csharp();

// 使用 torch.acos 计算夹角 (以弧度为单位)
var theta = torch.acos(cos);

// 将弧度转换为角度
var theta_degrees = torch.rad2deg(theta);
theta_degrees.print_csharp();
[], type = Float64, device = cpu, value = 0.70711
[], type = Float64, device = cpu, value = 45

image-20241108221229577


柯西-施瓦茨不等式

\(a\)

\(b\)
是两个向量,根据前面学到的投影和夹角知识,我们可以将以下公式进行转换。

\[\cos \varphi = \frac{\alpha \cdot \beta}{|\alpha|\cdot|\beta|}
\]

\[\alpha \cdot \beta = |\alpha|\cdot|\beta| \cos \varphi
\]

由于
\(-1 \le \cos \varphi \le 1\)
,所以:

\[- |\alpha|\cdot|\beta| \le \alpha \cdot \beta \le |\alpha|\cdot|\beta|
\]


这个就是 柯西-施瓦茨不等式。


也就是说,当两个向量的夹角最小时,两个向量的方向相同(角度为0),此时两个向量的乘积达到最大值,角度越大,乘积越小。在深度学习中,可以将两个向量的方向表示为相似程度,例如向量数据库检索文档时,可以算法计算出向量,然后根据相似程度查找最优的文档信息。

image-20241112112037795


向量的点积

点积即向量的数量积,点积、数量积、内积,都是同一个东西。

两个向量的数量积是标量,即一个数值,而向量积是不同的东西,这里只说明数量积。

数量积称为两个向量的数乘,而向量积才是两个向量的乘法。

向量的数乘公式如下:

\[a\cdot b=\displaystyle\sum_{i=1}^{n} a_{i} b_{i}=a_{1} b_{1}+a_{2} b_{2}+...+a_{n} b_{n}
\]


加上前面学习投影时列出的公式,如果可以知道向量的模和夹角,我们也可以这样求向量的点积:

\[\alpha \cdot \beta = |\alpha|\cdot|\beta| \cos \varphi
\]


例如 $\overrightarrow{AB} (2,4)
\(、\)
\overrightarrow{BC} (3,-1) $ 两个向量,如下图所示。

image-20241108205142069

计算其点积如下:

var B = torch.from_array(new[] { 2.0, 4.0 });
var A = torch.from_array(new[] { 3.0, -1.0 });

var dot = torch.dot(A, B);

dot.print_csharp();
[], type = Float64, device = cpu, value = 2


读者可以试试根据点积结果计算出
\(\angle ABC\)
的角度。


向量积

在画坐标轴时,我们默认轴上每个点间距都是 1,此时 x、y、z 上的单位向量都是 1,如果一个向量的模是 1,那么这个向量就是单位向量,所以单位向量可以有无数个。

image-20241113004516264


在数学中,我们往往会有很多未知数,此时我们使用
\(i\)

\(j\)

\(k\)
分别表示与 x、y、z 轴上正向一致的三个单位向量,
在数学和物理中,单位向量通常用于表示方向而不关心其大小
。不理解这句话也没关系,忽略。


在不关心向量大小的情况下,我们使用单位向量可以这样表示两个向量:

\[a = x_{1}i+y_{1}j+z_{1}k = (x_{1}, y_{1}, z_{1})
\]

\[b = x_{2}i+y_{2}j+z_{2}k = (x_{2}, y_{2}, z_{2})
\]


在三维空间中,
\(i\)

\(j\)

\(k\)
分别表示三个轴方向的单位向量。

  • \(i\)
    表示沿 x 轴方向的单位向量。
  • \(j\)
    表示沿 y 轴方向的单位向量。
  • \(k\)
    表示沿 z 轴方向的单位向量。

这种方式表示 a 在 x 轴上有
\(x_{1}\)
个单位,在 y 轴上有
\(y_{1}\)
个单位,在 z 轴上有
\(z_{1}\)
个单位。

一般来说,提供这种向量表示法,我们并不关心向量的大小,我们只关心其方向,如下图所示。

image-20241108223336564

现在我们来求解一个问题,在空间中找到跟 $\overrightarrow{AB}
\(、\)
\overrightarrow{BC} $ 同时垂直的向量,例如下图的 $\overrightarrow{AD} $,很明显,这样的向量不止一个,有无数个,所以我们这个时候要了解什么是法向量和单位向量。

image-20241113005446796

法向量是一个与平面垂直的向量(这里不涉及曲面、曲线这些),要找出法向量也很简单,有两种方法,一种是坐标表示:

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&x_{1} &y_{1} &z_{1} \\
&x_{2} &y_{2} &z_{2}
\end{vmatrix} = (y_{1}z_{2}-z_{1}y_{2})i - (x_{1}z_{2}-z_{1}x_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


这样记起来有些困难,我们可以这样看,容易记得。

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&x_{1} &y_{1} &z_{1} \\
&x_{2} &y_{2} &z_{2}
\end{vmatrix} = (y_{1}z_{2}-z_{1}y_{2})i + (z_{1}x_{2}-x_{1}z_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


那么法向量
\(n\)

\(x = (y_{1}{z2} -z_{1}y_{2})\)
,y、z 轴同理,就不给出了,x、y、z 分别就是 i、j、k 前面的一块符号公式,所以法向量为:

\[n(y_{1}z_{2}-z_{1}y_{2},z_{1}x_{2}-x_{1}z_{2},x_{1}y_{2}-y_{1}x_{2})
\]


任何一条下式满足的向量,都跟
\(a\)

\(b\)
组成的平面垂直。

\[c = (y_{1}z_{2}-z_{1}y_{2})i + (z_{1}x_{2}-x_{1}z_{2})j + (x_{1}y_{2}-y_{1}x_{2})k
\]


例题如下。

求与
\(a = 3i - 2j + 4k\)

\(b = i + j - 2k\)
都垂直的法向量 。

首先提取
\(a\)
在每个坐标轴上的分量
\((3,-2,4)\)
,b 的分量为
\((1,1,-2)\)

则:

\[a \times b =
\begin{vmatrix}
&i &j &k \\
&3 &-2 &4 \\
&1 &1 &-2
\end{vmatrix} = (4-4)i + (4-(-6))j + (3-(-2))k = 10j + 5k
\]

所以法向量
\(n(0,10,5)\)

这就是通过向量积求得与两个向量都垂直的法向量的方法。


你甚至可以使用 C# 手撸这个算法出来:

var A = torch.tensor(new double[] { 3.0, -2, 4 });

var B = torch.tensor(new double[] { 1.0, 1.0, -2.0 });
var cross = Cross(A, B);
cross.print();

static Tensor Cross(Tensor A, Tensor B)
{
    if (A.size(0) != 3 || B.size(0) != 3)
    {
        throw new ArgumentException("Both input tensors must be 3-dimensional.");
    }

    var a1 = A[0];
    var a2 = A[1];
    var a3 = A[2];
    var b1 = B[0];
    var b2 = B[1];
    var b3 = B[2];

    var i = a2 * b3 - a3 * b2;
    var j = a3 * b1 - a1 * b3;
    var k = a1 * b2 - a2 * b1;

    return torch.tensor(new double[] { i.ToDouble(), -j.ToDouble(), k.ToDouble() });
}
[3], type = Float64, device = cpu 0 -10 5

由于当前笔者所用的 C# 版本的 cross 函数不对劲,不能直接使用,所以我们也可以利用内核函数直接扩展一个接口出来。

public static class MyTorch
{
    [DllImport("LibTorchSharp")]
    public static extern IntPtr THSLinalg_cross(IntPtr input, IntPtr other, long dim);

    public static Tensor cross(Tensor input, Tensor other, long dim = -1)
    {
        var res = THSLinalg_cross(input.Handle, other.Handle, dim);
        if (res == IntPtr.Zero) { torch.CheckForErrors(); }
        return torch.Tensor.UnsafeCreateTensor(res);
    }
}
var A = torch.tensor(new double[] { 3.0, -2, 4 });

var B = torch.tensor(new double[] { 1.0, 1.0, -2.0 });

var cross = MyTorch.cross(A, B);
cross.print_csharp();
[3], type = Float64, device = cpu, value = double [] {0, 10, 5}


当前笔者所用版本 other 参数是 Scalar 而不是 Tensor,这里应该是个 bug,最新 main 分支已经修复,但是还没有发布。

image-20241109024627974


另外,还有一种通过夹角求得法向量的方法,如果知道两个向量的夹角,也可以求向量积,公式如下:

\[a \times b = |a| \cdot |b| \sin\alpha
\]


一般来说,对于空间求解问题,我们往往是可以计算向量积的,然后通过向量积得出
\(|a| \cdot |b| \sin\alpha\)
的结果,而不是通过
\(|a| \cdot |b| \sin\alpha\)
求出
\(a \times b\)

关于此条公式,这里暂时不深入。


直线和平面表示法

在本小节节中,我们将学习空间中的直线和平面的一些知识。

在空间中的平面,可以使用一般式方程表达:

\[v = Ax + By + Cz + D
\]


其中 A、B、C 是法向量的坐标,即
\(n = \{A,B,C\}\)


首先,空间中的直线有三种表示方法,分别是对称式方程、参数式方程、截距式方程。


直线的对称式方程

给定空间中的一点
\(P_{0}(x_{0},y_{0},z_{0})\)
有一条直线 L 穿过
\(p_{0}\)
点,以及和非零向量
\(v=\{l,m,n\}\)
平行。

image-20241109150817967


直线上任意一点和
\(p_{0}\)
的向量都和
\(v\)
平行,
\(\overrightarrow{P_{0}P} =\{x - x_{0},y - y_{0}, z - z_{0}\}\)
,所以其因为其对应的坐标成比例,所以其截距式方程为:

\[\frac{x-x_{0}}{l} = \frac{y-y_{0}}{m} =\frac{z-z_{0}}{n}
\]


直线的参数式方程

因为:

\[\frac{x-x_{0}}{l} = \frac{y-y_{0}}{m} =\frac{z-z_{0}}{n} = t
\]


所以:

\[\begin{cases}x = x_{0} + lt
\\y = y_{0} + mt
\\z = z_{0} + nt

\end{cases}
\]


这便是直线的参数式方程。

直线的一般式方程

空间中的直线可以看作是两个平面之间的交线,所以直线由两个平面的一般式方程给出:

\[\begin{cases}v_{1} = A_{1}x + B_{1}y + C_{1}z + D_{1}
\\ v_{2} = A_{2}x + B_{2}y + C_{2}z + D_{2}

\end{cases}
\]


这些公式在计算以下场景问题时很有帮助,不过本文不再赘述。


① 空间中任意一点到平面的距离。

② 直线和平面之间的夹角。

③ 平面之间的夹角。


矩阵

矩阵在在线性代数中具有很重要的地位,深度学习大量使用了矩阵的知识,所以读者需要好好掌握。

如下图所示,A 是一个矩阵,具有多行多列,
\(a_{11}、a_{12}、...、a_{1n}\)
是一个行,
\(a_{11}、a_{21}、...、a_{m1}\)
是一个列。

image-20240910115046782


在 C# 中,矩阵属于二维数组,即
\(m*n\)
,例如要创建一个
\(3*3\)
的矩阵,可以这样表示:

var A = torch.tensor(new double[,]
{
    { 3.0, -2.0, 4.0 },
    { 3.0, -2.0, 4.0 },
    { 3.0, -2.0, 4.0 }
});

A.print_csharp();


使用
.T
将矩阵的行和列倒过来:

var A = torch.tensor(new double[,]
{
    { 3.0, -2.0, 4.0 }
});

A.T.print_csharp();


生成的是:

{
	{3.0},
	{-2.0},
	{4.0}
}

如图所示:

image-20241109154450656


矩阵的加减

矩阵的加减法很简单,就是相同位置的数组加减。

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0, 4.0 },
    { 1.0, 2.0, 4.0 },
    { 1.0, 2.0, 4.0 }
});

var B = torch.tensor(new double[,]
{
    { 1.0, 1.0, 2.0 },
    { 1.0, 1.0, 2.0 },
    { 1.0, 1.0, 2.0 }
});

(A+B).print_csharp();

结果是:

{ 
    {2, 3, 6}, 
    {2, 3, 6}, 
    {2, 3, 6}
}


如果直接将两个矩阵使用 Pytorch 相乘,则是每个位置的数值相乘,这种乘法称为 Hadamard 乘积:

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0 }
});

var B = torch.tensor(new double[,]
{
    { 3.0, 4.0 }
});

// 或者 torch.mul(A, B)
(A * B).print_csharp();
[2x1], type = Float64, device = cpu, value = double [,] { {3}, {8}}


矩阵乘法

我们知道,向量内积可以写成
\(x_{2}x_{1}+y_{2}y_{1}+z_{2}z_{1}\)
,如果使用矩阵,可以写成:

\[\begin{bmatrix}
&x_{1} &y_{1} &z_{1} \\
\end{bmatrix} \cdot
\begin{bmatrix}
&x_{2} \\
&y_{2} \\
&z_{2}
\end{bmatrix} = x_{2}x_{1}+y_{2}y_{1}+z_{2}z_{1}
\]


换成实际案例,则是:

\[\begin{bmatrix}
&1 &2 &3\\
\end{bmatrix} \cdot
\begin{bmatrix}
&4 \\
&5 \\
&6
\end{bmatrix} = 1*4 + 2*5 + 3*6 = 32
\]


使用 C# 计算结果:

var a = torch.tensor(new int[] { 1, 2, 3 });
var b = torch.tensor(new int[,] { { 4 }, { 5 }, { 6 } });

var c = torch.matmul(a,b);
c.print_csharp();
[1], type = Int32, device = cpu, value = int [] {32}


上面的矩阵乘法方式使用 **A ⊗ B ** 表示,对于两个多行多列的矩阵乘法,则比较复杂,下面单独使用一个小节讲解。


**A ⊗ B **

矩阵的乘法比较麻烦,在前面,我们看到一个只有行的矩阵和一个只有列的矩阵相乘,结果只有一个值,但是对于多行多列的两个矩阵相乘,矩阵每个位置等于 A 矩阵行和 B 矩阵列相乘之和。


比如下面是一个简单的
2*2
矩阵。

\[\begin{bmatrix}
&a_{11} &a_{12} \\
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{11} &b_{12} \\
&b_{21} &b_{22}
\end{bmatrix}
=

\begin{bmatrix}
&c_{11} &c_{12} \\
&c_{21} &c_{22}
\end{bmatrix}
\]


因为
\(c_{11}\)
是第一行第一列,所以
\(c_{11}\)
是 A 矩阵的第一行乘以 B 第一列的内积。

\[c_{11} =
\begin{bmatrix}
&a_{11} &a_{12}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{11} \\
&b_{21}
\end{bmatrix}
= a_{11}*b_{11}+a_{12}*b_{21}
\]


因为
\(c_{12}\)
是第一行第二列,所以
\(c_{12}\)
是 A 矩阵的第一行乘以 B 第二列的内积。

\[c_{12} =
\begin{bmatrix}
&a_{11} &a_{12}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{12} \\
&b_{22}
\end{bmatrix}
= a_{11}*b_{12}+a_{12}*b_{22}
\]


因为
\(c_{21}\)
是第二行第一列,所以
\(c_{21}\)
是 A 矩阵的第二行乘以 B 第一列的内积。

\[c_{21} =
\begin{bmatrix}
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{22} \\
&b_{21}
\end{bmatrix}
= a_{21}*b_{11}+a_{22}*b_{21}
\]


因为
\(c_{22}\)
是第二行第二列,所以
\(c_{22}\)
是 A 矩阵的第二行乘以 B 第二列的内积。

\[c_{22} =
\begin{bmatrix}
&a_{21} &a_{22}
\end{bmatrix} \cdot
\begin{bmatrix}
&b_{12} \\
&b_{22}
\end{bmatrix}
= a_{21}*b_{12}+a_{22}*b_{22}
\]


例题如下:

\[\begin{bmatrix}
&1 &2 \\
&3 &4
\end{bmatrix} \cdot
\begin{bmatrix}
&5 &6 \\
&7 &8
\end{bmatrix}

=

\begin{bmatrix}
&(1*5 + 2*7) &(1*6 + 2*8) \\
&(3*5 + 4*7) &(3*6 + 4*8)
\end{bmatrix}
=
\begin{bmatrix}
&19 &22 \\
&43 &50
\end{bmatrix}
\]


使用 C# 计算多行多列的矩阵:

var A = torch.tensor(new double[,]
{
    { 1.0, 2.0 },
    { 3.0, 4.0 }
});

var B = torch.tensor(new double[,]
{
     { 5.0 , 6.0 },
     { 7.0 , 8.0 }
});

torch.matmul(A, B).print_csharp();
{ {19, 22}, {43, 50}}

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: AlignSum: Data Pyramid Hierarchical Fine-tuning for Aligning with Human Summarization Preference

创新点


  • 发现在文本摘要任务中,预训练语言模型在自动评估与人工评估中表现不一致,原因可能是低质量的训练数据。
  • 考虑到标注成本,论文提出了一种新的人类摘要偏好对齐框架
    \({\tt AlignSum}\)
    ,使用提取、
    LLM
    生成和人工标注等多种方法构建数据金字塔,能够充分利用极其有限的高质量数据来提升预训练语言模型(
    PLMs
    )在摘要生成方面的能力极限。

内容概述


文本摘要任务通常使用预训练语言模型(
PLMs
)来适应各种标准数据集。尽管这些
PLMs
在自动评估中表现出色,但在人工评估中常常表现不佳,这表明它们生成的摘要与人类摘要偏好之间存在偏差。这种差异可能是由于低质量的微调数据集,或者是能反映真正的人类偏好的高质量人类标注数据有限。

注释大量高质量摘要数据集是不切实际的,论文希望不再依赖于对大量训练数据进行传统的简单微调,而是充分利用极其有限的高质量数据来提升预训练语言模型(
PLMs
)在摘要生成方面的能力极限。

为了解决这个挑战,论文提出了一种新的人类摘要偏好对齐框架
\({\tt AlignSum}\)
。该框架由三个部分组成:首先,构建一个数据金字塔,其中包含抽取式、生成式和人类标注的摘要数据。其次,进行高斯重采样,以去除极端长度的摘要。最后,在高斯重采样后实现两阶段的分层微调与数据金字塔的结合。


\({\tt AlignSum}\)
应用到人类标注的
CNN
/
DailyMail

BBC XSum
数据集中,像
BART-Large
这样的
PLMs
在自动评估和人工评估中都超越了
175B

GPT-3
。这证明了
\({\tt AlignSum}\)
显著增强了语言模型与人类摘要偏好的对齐。

AlignSum


整体框架包含三个部分:

  1. 使用提取、
    LLM
    生成和人工标注等多种方法构建数据金字塔(
    Data Pyramid
    )。
  2. 由于源数据具有不同的摘要长度,利用高斯重新采样来调整生成摘要的长度,以接近目标长度。
  3. 采用了两阶段的层次微调策略:初始阶段对
    PLMs
    进行抽取式和生成式数据的训练,以适应一般领域,然后在人工标注数据上对刚刚微调过的
    PLMs
    进行进一步微调,以使其与人类偏好对齐。

构建数据金字塔

数据金字塔由三个层级组成,从下到上按质量和获取难度递增,而数量则递减。前两者是摘要生成领域中最常见的两种风格,将它们统称为通用数据。最后一层是用于对齐人类偏好的最关键部分,称之为个性化数据。

  • 抽取式数据

抽取式数据构成了预训练语料库的主要部分,并且是最容易获得的。参考
GSG
,使用
ROUGE-1
指标来计算相似性,并遍历整个文档以找到与之最相似的句子作为伪摘要
\(\hat{S}\)

\[\begin{equation}
\begin{split}
&\ \ r_i = \mathrm{Rouge} (d_i, D_{\setminus d_i}), \\
&\ \ \hat{S} = \mathrm{argmax}_{d_i} \{r_i\}_{i=1}^n.
\end{split}
\end{equation}
\]

  • 生成式数据

抽取式数据有助于识别文档中的重要句子,但不足以总结跨越多个句子的关键信息。相比之下,
LLMs
(大规模语言模型)是有效的零样本摘要生成器,能够提取跨句子及文档级别的摘要信息。

使用系统提示和用户提示引导
LLMs
对文档
\(D\)
进行摘要,并生成伪摘要
\(\hat{S}\)
。系统提示指定了准确摘要生成的一般要求,然后在用户提示之前插入文档,确保
LLM
能够阅读整个文档并遵循用户要求。用户提示是数据集特定的,设定所需的摘要长度和单词数量。

  • 人类标注数据

通过使用上述两种数据进行训练,
PLMs
(预训练语言模型)获得了领域特定的知识。为了生成符合人类偏好的摘要,进一步在人类标注数据上进行微调是必要的。

为了避免随机注释的差异性,使用
Element-aware
数据集。该数据集遵循特定指令,结合了微观和宏观需求,确保一致且高质量的人类注释。

高斯重采样

三个不同的数据源的伪摘要都有独特的标记长度分布,其中抽取式和抽象数据的摘要标记长度分布存在明显差异。因此,直接使用这些不同的分布进行训练可能会导致生成过长或过短的摘要。

为了解决这个问题,引入了高斯重采样技术,以使所有摘要长度与人类注释的摘要对齐。

将人类标注数据的标记长度分布建模为高斯分布。在
95%
概率的 [
\(\mu - 2\sigma\)
,
\(\mu + 2\sigma\)
]区间内对抽取式和抽象数据进行重采样,以去除具有过长或过短伪摘要的样本。

两阶段层级微调

直接对预训练语言模型(
PLMs
)进行微调可能会很具挑战性,因为少量的高熵数据对于对齐至关重要,但可能会受到大量低熵数据的信息干扰,从而导致数据金字塔的未充分利用。

为了避免这个潜在问题,论文提出两阶段的分层微调策略。给定一个预训练语言模型
\(p_{\theta}\)

  1. 首先通用微调阶段,使用抽取式和抽象数据对
    \(p_{\theta}\)
    进行微调,以增强其生成领域通用摘要的能力,从而获得模型
    \(p_{\theta'}\)
  2. 接下来是个性化微调阶段,使用人类标注数据对
    \(p_{\theta'}\)
    进行微调,以创建与人类偏好对齐的最终模型
    \(p_{\theta''}\)

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

在微信群里看到有同学对.NET 9的贡献者数量有质疑,.NET 这样的一个全场景的应用开发平台,他的生态是很庞大的,自然一起参与开源贡献的开发者也是很大的,但是很多人都不知道一直有这么一个地址是统计了.NET各个版本的开发者数量的,这篇文章就是给大家统计显示一下各个版本的.NET贡献者人数.

.NET  Core 1.0 一共有12870 贡献:
https://dotnet.microsoft.com/en-us/thanks/1.0

image

.NET Core 2.0 一共有618 贡献:
https://dotnet.microsoft.com/en-us/thanks/2.0

image

.NET Core 3.0 一共有34108贡献:
https://dotnet.microsoft.com/en-us/thanks/3.0

image

.NET Core 3.1一共有9491 贡献:
https://dotnet.microsoft.com/en-us/thanks/3.1

image

.NET 5.0一共有49900 贡献:
https://dotnet.microsoft.com/en-us/thanks/5.0

image

.NET 6.0一共有243366 贡献:
https://dotnet.microsoft.com/en-us/thanks/6.0

image

.NET 7.0 一共有155976 贡献:
https://dotnet.microsoft.com/en-us/thanks/7.0

image

.NET 8.0 一共有78000 贡献:
https://dotnet.microsoft.com/en-us/thanks/8.0

image

.NET 9一共有49946 贡献:
https://dotnet.microsoft.com/en-us/thanks/9.0

image

数据展示组件在
Streamlit
各类组件中占据了至关重要的地位,

它的核心功能是以直观、易于理解的方式展示数据。

本次介绍的数据展示组件
st.dataframe

st.table
,能够将复杂的数据集以表格、图表等形式清晰地呈现出来,使得用户能够快速把握数据的整体情况和细节特征。

1. st.dataframe

st.dataframe
以易读且美观的方式展示
pandas

DataFrame

无论是处理小型数据集还是庞大的数据表,
st.dataframe
都能轻而易举展示数据。

st.dataframe
适用于需要在Web应用中展示复杂数据集的场景。

首先,它能够自动适应屏幕宽度,并支持水平或垂直滚动,确保用户能方便地浏览整个数据集。

此外,
st.dataframe
还支持对数据进行排序、筛选和搜索等操作,增强了数据的可读性和交互性。

2. st.table

st.table
也是用于在Web应用中显示表格数据,

它可以显示交互式表格,并提供多种自定义设置来满足各类需求。


st.dataframe
相比,
st.table
更适用于当
数据集不是特别庞大
且需要保持清晰可读性的场景。

它允许用户通过简单的配置来调整表格的显示方式,如列宽、行高等。

3. 两者区别

这两个组件都用于展示数据,都支持多种类型的数据对象作为输入,比如
pandas.DataFrame

numpy.ndarray

Iterable

dict
等等。

但是在
交互性

显示方式

功能丰富度
上面是有区别的,

下面通过一个示例来演示两者在使用上的区别,

先使用
st.dataframe
显示一个包含用户信息的静态
DataFrame
,如姓名、年龄和邮箱。

DataFrame
将显示为可滚动、可排序和可搜索的表格。还可以将数据保存为
CSV
文件。

同样使用 st.table 显示相同的用户信息数据集,但表格样式会更加简洁,功能相对较少(例如,不支持搜索)。

import streamlit as st
import pandas as pd

# 创建静态数据集
data = {
    "姓名": ["张三", "李四", "王五"],
    "年龄": [25, 30, 35],
    "邮箱": ["zhangsan@example.com", "lisi@example.com", "wangwu@example.com"],
}
df = pd.DataFrame(data)

st.header("st.dataframe")
# 使用st.dataframe显示
st.dataframe(df)

st.header("st.table")
# 使用st.table显示
st.table(df)

除了功能比较丰富以外,
st.dataframe
对于展示千上万行的大型数据集时,可以调整其高度和宽度,可以搜索过滤和排序,因此更方便遇查看数据。


st.table
由于功能相对简单,会将所有数据直接展示出来,浏览和分析大量数据不那么方便。

比如,下面模拟了一个一万条数据的场景。

st.dataframe
展示时,可以固定一块位置;而
st.table
将所有数据平铺下去展示,加装时间也明显长很多。

# 创建大数据集
np.random.seed(0)
data = {
    "ID": np.arange(1, 10001),
    "值1": np.random.rand(10000),
    "值2": np.random.rand(10000),
    # ... 可以添加更多列
}
df = pd.DataFrame(data)

st.header("st.dataframe", width=400, height=600)
# 使用st.dataframe显示大数据集
st.dataframe(df)

st.header("st.table")
# 使用st.table显示大数据集(可能性能不佳)
# 对于大数据集,st.table可能不是最佳选择
st.table(df)

4. 总结

总得来看,
st.dataframe
更适合需要高级功能和动态交互的场景,


st.table
则更适合简单、快速的表格展示。

基本概念

基于磁盘的B+树

为什么使用B+数进行数据访问(Access Method):

image-20241109143725399

  • 天然有序,支持范围查找
  • 支持遍历所有数据,利用顺序IO
  • 时间复杂度为
    \(O(logn)\)
    ,满足性能需求
  • 相比于B树,数据访问都在叶子结点:磁盘空间利用率高;并发冲突减少

一个基础的B+树:

  • 三类借点:根结点,中间结点,叶子结点
  • 数据分布:根结点和中间结点只存储索引,叶子结点存储数据
  • 指针关系:父子指针,兄弟指针

image-20241113104944036

基于磁盘的B+树映象:

一个结点存储在一个堆文件(Heap File)页中;页ID(PageId)代替指针的作用。

  • 键值联合存储

    image-20241113105746186

  • 键值分别存储

    image-20241113105801713

B+树的叶子结点存储实际数据,这个数据如何理解,取决于不同的数据库实现:有些存储RecordID,有些基于索引组织(Index-Organized Storage)的数据库则直接存储元组(Tuple)数据。

image-20241113110032259

如果不了解RecordID,数据组织方式,可以参看
这篇博文

查询与索引

最左前缀匹配

有联合索引
<a,b,c>
,支持如下查询条件

  • (a=1 AND b=2 AND c=3)
  • (a=1 AND b=2)

image-20241113111703944

如果所有不满足最左前缀匹配原则,需要全表扫描。

如何处理重复键

  • 加上RecordID使其变成唯一键

    image-20241113111945385

  • 叶子结点溢出(没有实际系统采用)

    image-20241113111956652

聚簇索引

  • 一个表只能有一个聚簇索引
  • 索引键和值存储在一起
  • 数据按照索引的键排序
  • 操作数据时要同步操作索引

聚簇索引是非必须的,取决于数据库具体实现,Mysql和SQLite中数据直接用聚簇索引组织。

用B+树实现聚簇索引可以很方便地实现范围查询和便利,充分利用顺序IO。

image-20241113112518741

对于非聚簇索引,虽然索引的键有序,但是对应的数据在磁盘上不一定是顺序存储的,所以很有效的方式是先得到PageID,后根据PageID进行排序,最后获取数据,充分利用顺序IO。
image-20241113112630957

设计选择

结点大小(Node Size)

存储设备读取数据越慢,越需要利用顺序IO,结点就越大;

存储设备读取数据越快,越需要减少冗余数据读取,结点就越小。

  • HDD:~1MB
  • SSD:~10KB
  • In-Memory:~512B

合并阈值(Merge Thredshold)

结点中的键数量低于半满的时候,不会立刻进行合并,而是允许小结点存在,然后再周期性地重建整棵树。

PostgreSQL中称其为不平衡的B+树("non-balanced" B+Tree, nbtree)。

变长键(Variable-length Keys)

  • 指针:键存储指向实际数据的指针【无法利用顺序IO,因为要跳转去读取指针内容】
  • 变长结点
  • 填充数据(Padding)

实际系统中的索引数据和堆文件数据一样,是能存结点就存结点中,是在存不下存指针。

  • 线性查找:由于SIMD指令集存在,实际顺序查询,其实可以是批处理


    image
  • 二分查找

  • 插值法:键没有间隙的时候(自增),可以直接计算出偏移


    image

优化手段

Pointer Swizzling

基本思想:当一个对象从磁盘加载到内存时,将其磁盘地址转换成内存地址(
swizzling
),以便程序在内存中直接通过指针访问。

例子:比如主键索引的B+树根结点读取到Buffer Pool后,会被pin住,不被置换出去,所以此时可以直接用内存指针访问根结点,省略用PageID问Buffer Pool要内存地址的步骤。

图示:

image
image

Bε-trees

一种B+树的写优化。

基本思想:更新时不直接修改数据 ,而是记录日志(类似于log-structured data storage)。

日志记录在结点上,当结点日志记录满以后,该结点的日志下推到孩子结点。

image-20241113133509231

image-20241113133530151

Bulk Insert

基本思想:由底至顶创建B+树,而不是由顶至底。

减少了插入时树的结构变化,前提是需要预先排序数据。

Keys: 3, 7, 9, 13, 6, 1
Sorted Keys: 1, 3, 6, 7, 9, 13

image-20241113133953890

image-20241113134001752

Prefix Compression

基本思想:字典序压缩前缀。

Deduplication

基本思想:非唯一索引中避免重复存储相同键。

Suffix Truncation

基本思想:中间结点只是起引路作用,所以存储能辨识的最小前缀即可。

image
image