线代第一章:向量

向量


真实世界里的数字是有方向的。

我印象最深刻的是神作《天蝎》的里剧情。

主人公和匪徒在天台上追逐,可匪徒失足了,幸好还有一只手扣住了墙。

主人公并没有救,因为主人公:

  • 重 72 公斤,俩臂伸展为 1.7 米,1.8米的个子,弯腰到 90 度;

而匪徒:

  • 90 公斤。

那,主人公能拉起匪徒吗?

因为俩个人的用力的方向呈 90 度角,不是简单的合力。

扫描二维码关注公众号,回复: 10746141 查看本文章

所以不仅不能,而且可能会被拖下去。

真实世界是有方向的,数学要研究真实世界,那绝对不能只是看数值大小,还得关心方向。

描述带方向的数字的工具,叫【向量】,如果只关心数值,不关心方向的数量叫【标量】。

因为我们的世界是 3 维的,所以用 3 个向量就可以研究我们的世界了。

不过在一些虚拟的世界里不止,比如游戏的血量、蓝量、攻击、防御等等。

这时候就需要向量的形式化定义了:

代码实现向量的初始化:

#include<stdio.h>
#include<malloc.h>

typedef int T;
typedef struct _vector{                 // 向量建模
   T *arr;     // 向量是一组数字,用数组描述很方便
   int len;
}vector;

void init( vector * v )                 // 初始化为0
{
	printf("向量维度:>  ");
	scanf("%d", &v->len);
	v->arr = (T*)malloc(sizeof(T) * v->len);
	for( int i = 0; i < v->len; i++ )
	    v->arr[i] = 0;
}

void print(vector *v){                  // 打印向量
    for( int p=0; p<v->len; p++ )
        printf("%d  ", v->arr[p]);
    putchar(10);
}

T get_index_val(vector *v, int index)   // 获取第 n 个向量 
{
	return v->arr[index];
}

int get_len( vector *v )                // 获取向量的个数
{
	return v->len;
}

int main(){
   vector v;
   init(&v);
   print(&v);
}                                 

向量的运算


向量加法

向量加法

int add(int *arr_1, int *arr_2)
{
	int len = sizeof(arr_1)/sizeof(arr_1[0]);   // 获取向量长度,默认俩个向量同维

	static int sum = 0;
	for( int i=0; i<len; i++ )
	    sum += arr_1[i] + arr_2[i];
	return sum;
}

数量乘法

static 
int mul(int *arr_1, int *arr_2)
{
	int len = sizeof(arr_1)/sizeof(arr_1[0]);   // 获取向量长度,默认俩个向量同维
	static int mul = 0;
	for( int i=0; i<len; i++ )
	    mul *= arr_1[i] + arr_2[i];
	return mul;
}

向量的模

根据向量坐标点求长度/模:

二维平面可以通过【勾股定理】就求出长度为 5。

  • 二维向量的模: u = a 2 + b 2 \left \|\underset{u}{\rightarrow} \right \| = \sqrt{a^{2}+b^{2}}

三维平面可以拆分为一个二维平面和一个维平面,比如先计算二维平面的 O A \left \| \underset{OA}{\rightarrow} \right \|

O A = 2 2 + 3 2 \left \| \underset{OA}{\rightarrow} \right \| = \sqrt{2^{2}+3^{2}}

O P = O A 2 + A P 2 = 2 2 + 3 2 + 5 2 \left \| \underset{OP}{\rightarrow} \right \|=\sqrt{\left \| \underset{OA}{\rightarrow} \right \|^{2} +\left \|\underset{AP}{\rightarrow} \right \|^{2} }=\sqrt{2^{2}+3^{2}+5^{2}}

u = 2 2 + 3 2 + 5 2 \left \| \underset{u}{\rightarrow} \right \| = \sqrt{2^{2}+3^{2}+5^{2}}

  • 三维向量的模: u = a 2 + b 2 + c 2 \left \| \underset{u}{\rightarrow} \right \| = \sqrt{a^{2}+b^{2}+c^{2}}

一般化后,n维向量的模:

u = ( u 1 , u 2 , . . . , u n ) T \left \| \underset{u}{\rightarrow} \right \| = (u_{1}, u_{2},..., u_{n})^{T}

  • n维向量的模: u = u = u 1 2 + u 2 2 + . . . + u n 2 \left \| \underset{u}{\rightarrow} \right \| = \left \| \underset{u}{\rightarrow} \right \| = \sqrt{u_{1}^{2}+u_{2}^{2}+...+u_{n}^{2}}
double norm( T *arr, int n )  // 向量模
{
	double norm_result = 0.0f;
	for( int i=0; i<n; i++ )
	    norm_result += sqrt(pow(arr[i], 2));
	
	return norm_result;
}

单位向量

单位向量:长度为 1 的向量, u = 1 \left \| \underset{u'}{\rightarrow} \right \|=1

每个向量都有一个单位向量,那怎么求呢?

1 u u = ( u 1 u ,   u 2 u ,   u 3 u , ,   u n u ) \frac{1}{\left \| \underset{u}{\rightarrow} \right \|}*\underset{u}{\rightarrow}=(\frac{u_{1}}{\left \| \underset{u}{\rightarrow} \right \|},~\frac{u_{2}}{\left \| \underset{u}{\rightarrow} \right \|},~\frac{u_{3}}{\left \| \underset{u}{\rightarrow} \right \|},\cdots ,~\frac{u_{n}}{\left \| \underset{u}{\rightarrow} \right \|})

根据 u \underset{u}{\rightarrow} u \underset{u'}{\rightarrow} 的过程叫【归一化】、【规范化】(normalize)。

单位向量其实有无数多个,但在二维平面中有俩个单位向量比较特殊,就是 x y x、y 坐标正方向上的俩个。

e 1 = ( 1 , 0 ) \underset{e_{1}}{\rightarrow}=(1, 0)

e 2 = ( 0 , 1 ) \underset{e_{2}}{\rightarrow}=(0, 1)

只由 0、1 组成,因此也叫标准单位向量。

同理,三维空间中就有 3 个标准单位向量。

double normalize( T *arr, int n )
{
	double normalize_result[n];
	for( int i=0; i<n; i++ )
	    normalize_result[i] = arr[i]/norm(arr, sizeof(arr)/sizeof(arr[0]));
}

零向量

零向量:起点和终点为同一个点的向量,在几何上是原点。

T *zero( int N )  // 传入维度n,返回 n 维零向量
{
	T *p = (T*)malloc(sizeof(T) * N);
	for( int i=0; i<N; i++ )
	    p[i] = 0;
	return p;
}

反证法证明:存在零向量。

对于任意一个向量 u \underset{u}{\rightarrow} ,都存在一个向量 u \underset{-u}{\rightarrow} , 满足 u + u = O \underset{-u}{\rightarrow}+\underset{u}{\rightarrow}=O ,上述 u \underset{-u}{\rightarrow} 唯一。

假设存在另一向量 v \underset{v}{\rightarrow} ,也满足 v + u = O \underset{v}{\rightarrow}+\underset{u}{\rightarrow} = O

可以建立一个等式:

( v + u ) + u = u + O (\underset{v}{\rightarrow}+\underset{u}{\rightarrow})+\underset{-u}{\rightarrow}=\underset{-u}{\rightarrow}+O

u + u + v = u \underset{-u}{\rightarrow}+\underset{u}{\rightarrow}+\underset{v}{\rightarrow}=\underset{-u}{\rightarrow}

v = u \underset{v}{\rightarrow}=\underset{-u}{\rightarrow}


点乘

点乘有俩种实现方法,这是上述公式的前一个:

long long dot( T * arr_1, T * arr_2, int len )
{
	long long sum = 0;
	for( int i=0; i<len; i++ )
	    sum += arr_1[i] + arr_2[i];
	    
	return sum;
}

Python版完整代码:

class Vector:

    def __init__(self, lst):
        self._values = list(lst)

    @classmethod
    def zero(cls, dim):
        """返回一个dim维的零向量"""
        return cls([0] * dim)

    def __add__(self, another):
        """向量加法,返回结果向量"""
        assert len(self) == len(another), \
            "Error in adding. Length of vectors must be same."

        return Vector([a + b for a, b in zip(self, another)])

    def __sub__(self, another):
        """向量减法,返回结果向量"""
        assert len(self) == len(another), \
            "Error in subtracting. Length of vectors must be same."

        return Vector([a - b for a, b in zip(self, another)])

    def __mul__(self, k):
        """返回数量乘法的结果向量:self * k"""
        return Vector([k * e for e in self])

    def __rmul__(self, k):
        """返回数量乘法的结果向量:k * self"""
        return self * k

    def __pos__(self):
        """返回向量取正的结果向量"""
        return 1 * self

    def __neg__(self):
        """返回向量取负的结果向量"""
        return -1 * self

    def __iter__(self):
        """返回向量的迭代器"""
        return self._values.__iter__()

    def __getitem__(self, index):
        """取向量的第index个元素"""
        return self._values[index]

    def __len__(self):
        """返回向量长度(有多少个元素)"""
        return len(self._values)

    def __repr__(self):
        return "Vector({})".format(self._values)

    def __str__(self):
        return "({})".format(", ".join(str(e) for e in self._values))

Numpy 向量的基本使用

Python的 list 元素可以是任意练习,设计理念主要用于存储,而 numpy 的作用却不是存储,而是计算。

Numpy 元素就只能是一种。

import numpy as np
# 导入 numpy 并取 np 这个别名,方便调用

vec1 = np.array([1, 2, 3])
# 传一个列表初始化,形成向量,也可以 np.zeros(n)

# 基本属性
print(vec1.size)
# 或print(len(vec1))

# 基本运算
vec2 = np.array([4, 5, 6])
print("{} + {} = {}".format(vec1, vec2, vec1+vec2))
print("{}".format(vec1.dot(vec1)))
# 点乘

print(np.linalg.norm(vec1))
# 模, linalg 是线代包
print(vec1/np.linalg.norm(vec1))
# 归一化

向量应用:新闻分类自动化

现在浏览器上的新闻,都是计算机自动分类的。

计算机分类的原理是三角函数的余弦定理 + 向量。

原理:余弦定理可以只靠俩个三角形的俩个边的向量,计算出这俩个边的夹角。

一篇新闻里会有很多词,像 “之乎者也的” 这种虚词,对判断新闻的分类没有太大的意义。而像 “股票”、“利息” 这种实词,是判断新闻分类的重点词。

科学家精选了一个词汇表,这里面收录着 64000 个词,每个词都对应一个编号。他们先把大量文字数据输入计算机,算出每个词出现的次数。

一般出现次数越少的词越有搜索价值,比如 “爱因斯坦”、“某个人名”;而出现次数越多的词,越没有搜索价值,比如“一个”、“这里” 等等。

根据这个标准,把词汇表里的64000个词都算出各自的权重,越特殊的词权重越大。

然后,再往计算机里输入要分类的新闻,计算出这64000个词在这篇新闻里的分布,如果某些词没有在这篇新闻里出现,对应的值就是零,如果出现,对应的值就是这个词的权重。

这样,这64000个数,就构成了一个64000维的向量,我们就用这个向量来代表这篇新闻,把它叫做这篇新闻的特征向量。

不同类型的新闻,用词上有不同的特点,比如金融类新闻就经常出现 “股票”、“银行” 这些词,所以不难判断,同类新闻的特征向量会有相似性。

只要算出不同新闻特征向量之间夹角的大小,就可以判断出是不是同一类新闻。

这时就要用到余弦定理,来把两则新闻的特征向量之间的夹角算出来。

科学家可以人工设定一个值,只要两个向量之间的夹角小于这个值,这两则新闻就可以判定成同一类新闻。

在向量中公式转换为:

把公式翻译为代码:

double CosSimilarity(double *va, double *vb, int vn) // vn 是多少维,也就是词典中有多少个词
{
    double cossu = 0.0;
    double cossda = 0.0;
    double cossdb = 0.0;
 
    for (int i = 0; i < vn; i++)
    {
        cossu += va[i] * vb[i];
        cossda += va[i] * va[i];
        cossdb += vb[i] * vb[i];
    }
 
    return cossu / (sqrt(cossda) * sqrt(cossdb));
}

完整代码:

#include <stdio.h>
#include <math.h>
 
double CosSimilarity(double *va, double *vb, int vn)
{
    double cossu = 0.0;
    double cossda = 0.0;
    double cossdb = 0.0;
 
    for (int i = 0; i < vn; i++)
    {
        cossu += va[i] * vb[i];
        cossda += va[i] * va[i];
        cossdb += vb[i] * vb[i];
    }
 
    return cossu / (sqrt(cossda) * sqrt(cossdb));
}
 
// 建立的词典
const int VN = 11;      // 11 个词即 11维
const char *base_words[] = 
{
    "进攻", "炮弹", "射程", "榴弹炮", "发射", "口径", "迫击炮", "瞄准", "后坐力", "弹道", "目标"
};
 
/* 原文 */
//第一行: 口径为155毫米的榴弹炮,炮弹的射程超过40公里,炮弹发射后击中目标的弹道是一条抛物线
//第二行: 大口径榴弹炮射程很远且弹道弯曲,炮弹通常都不是直接对着目标瞄准,而是计算好抛物线弹道,以一定的仰角和方向发射炮弹
//第三行: 我们必须统一口径,抵挡敌人发射的糖衣炮弹的进攻
 
int main()
{
	  // v1代表原文第一行的 11 个关键字出现的权重(一一对应, 出现n次权重为n)
    double v1[] = { 0, 2, 1, 1, 1, 1, 0, 0, 0, 1, 1 };
    double v2[] = { 0, 2, 1, 1, 1, 1, 0, 1, 0, 2, 1 };
    double v3[] = { 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0 };
 
    /* 检查相似度 */
    printf("第一行 和 第二行 的相似度: %.2lf\n", CosSimilarity(v1, v2, VN));
    printf("第一行 和 第三行 的相似度: %.2lf\n", CosSimilarity(v1, v3, VN));
    printf("第二行 和 第三行 的相似度: %.2lf\n", CosSimilarity(v2, v3, VN));
    
    // 代码还可补充,当相似度大于设定值时,归为一类新闻...... 
    return 0;
}

向量应用:筛选简历自动化

向量不仅可以对新闻分类,对人也可以分类。

现在大公司在招聘伙伴时,由于简历特别多,会先用计算机筛选简历。

原理:先把简历向量化,而后计算夹角。

把各种技能和素质列在一张表里,这个表就有 N 个维度啦。

而后不同岗位因为评比的方式不同,某些向量的权值就很高,一些就很低甚至是零。

比如,开发人员的权值:

  • 编程能力:4
  • 工程经验:2
  • 沟通能力:1
  • 学历:1
  • 领导力:1
  • 企业文化融合度:1

接下来计算机会对每份简历进行分析,把每份简历变成一个 N 维的向量,假设是 P。

计算 P、V 的夹角,如果夹角非常小说明某一份简历和某一个岗位比较匹配,这时简历才会递给HR。

所以,写给大公司的简历,一定要突出重点。别把自己描述为全能的,不然直接被计算机卡死啦,最好是先打探内部消息,知道这家公司看重什么维度,您再往上面写。
 


余弦定理

新闻分类、筛选简历自动化,关键在于算出俩个向量的夹角。

而算出俩个向量的夹角,只需要中学学过的余弦定理即可。

我们可以简单的推导一下:【余弦定理】。

余弦定理是从毕达哥拉斯定理推过来的,就是 a 2 + b 2 = c 2 a^{2}+b^{2}=c^{2}

数学家就是喜欢研究,他们发现:

  • 如果 a 和 b 的夹角大于 90度, a 2 + b 2 < c 2 a^{2}+b^{2}<c^{2}
  • 如果 a 和 b 的夹角小于 90度, a 2 + b 2 > c 2 a^{2}+b^{2}>c^{2}

对比一下 a 2 + b 2 c 2 a^{2}+b^{2}、c^{2} 就知道夹角是什么样的角了。

c 2 c^{2} 移到等式左边, a 2 + b 2 c 2 = 0 a^{2}+b^{2}-c^{2}=0

将等式左边作为判定因子 Δ \Delta ,用 Δ \Delta 0 0 比较大小,可以判定夹角:

  • Δ > 0 \Delta > 0 ,锐角;
  • Δ = 0 \Delta = 0 , 直角;
  • Δ < 0 \Delta < 0 ,钝角。

为了消除边长的影响,将 Δ \Delta 除以 夹角的俩个边长(a、b)的积,使得 Δ \Delta 的动态范围是在 [ 2 , + 2 ] [-2, +2]

Δ = 2 \Delta = -2 时,夹角最大,是 180度。

Δ = 0 \Delta = 0 时,是 90度。

如果再除以 2, Δ \Delta 的动态范围是在 [ 1 , + 1 ] [-1, +1] ,这个数值就等于夹角的余弦。

这种从毕达哥拉斯定理出发,建立角度判定因子 Δ \Delta 和具体角度的关系,就是我们所学的余弦定理。

发布了132 篇原创文章 · 获赞 377 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_41739364/article/details/105440872
今日推荐