• Articles posted by Conan Zhang

Author Archives: Conan Zhang

About Conan Zhang

Programmer(Java,R,PHP,Javascript)

用R语言手搓马尔可夫决策过程markov

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹,分析师/程序员/Quant: R,Java,Nodejs
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-markov/

前言

马尔科夫决策过程是强化学习的数学框架基础,通过状态、动作、奖励和状态转移概率的形式化描述,为智能体提供了与环境交互的理论模型。在强化学习中,MDP为价值函数、策略优化和贝尔曼方程等关键概念提供了理论基础,是理解Q学习、策略梯度等算法的重要前提,帮助智能体学习在不确定环境中做出最优决策。

新的方向,开始强化学习的学习。

目录

  1. 马尔可夫(Markov)决策过程介绍
  2. 用R语言实现马尔可夫链

1. 马尔可夫(Markov)决策过程介绍

马尔可夫决策过程(Markov decision process, MDP),是一种关于事件发生的概率预测方法。它是根据事件的目前状况来预测其将来各个时刻(或时期)变动状况的一种预测方法。马尔可夫决策过程包含一组交互对象,即智能体(agent)和环境,并定义了5个模型要素:状态(state)、动作(action)、策略(policy)、奖励(reward)和回报(return),其中策略是状态到动作的映射,回报是奖励随时间步的折现或积累。

马尔可夫决策过程于随机过程领域,随机过程与概率论中静态的随机现象不同,随机过程的研究对象是随时间演变的随机现象(例如天气随时间的变化、城市交通随时间的变化)。在随机过程中,随机现象在某时刻的取值是一个向量随机变量,用表示,所有可能的状态组成状态集合。随机现象便是状态的变化过程。在某时刻的状态St通常取决于t时刻之前的状态。

状态转移

马尔可夫过程举例,一个具有 6 个状态的马尔可夫过程的例子。

其中每个绿色圆圈(s1-s6)表示一个状态,每个状态都有一定概率(包括概率为 0)转移到其他状态。其中s6 被称为终止状态,它不再转出到其他状态。状态之间的虚线箭头表示状态的转移,箭头旁的数字表示该状态转移发生的概率。从每个状态出发转移到其他状态的概率总和为 1。例如,有s1有 90%概率到自己保持不变,s1有 10%概率转移到s2。而在s2又有 50%概率回到s1,s2有 50%概率转移到s3。

把上面的状态转移过程,用矩阵表示,即状态转移矩阵。其中,矩阵第i行j列的值,则代表从状态Si转移到Sj的概率。

给定一个马尔可夫过程,我们就可以从某个状态出发,根据它的状态转移矩阵生成一个状态序列(episode),这个步骤也被叫做采样(sampling)。例如,从S1出发,可以生成序列S1->S2->S3->S6,或S1->S1->S2->S3->S4->S5->S3->S6等。生成这些序列的概率和状态转移矩阵有关。

马尔可夫的奖励过程

在马尔可夫过程的基础上加入奖励函数r 和 折扣因子γ,就可以得到马尔可夫奖励过程(Markov reward process)。一个马尔可夫奖励过程由(S,P,r,γ)构成,各个组成元素的含义如下所示。

  • S是有限状态的集合。
  • P是状态转移矩阵。
  • r是奖励函数,某个状态s的奖励r(s) 指转移到该状态时可以获得奖励的期望。
  • γ是折扣因子(discount factor),γ的取值范围为[0,1)。引入折扣因子的理由为远期利益具有一定不确定性,有时我们更希望能够尽快获得一些奖励,所以我们需要对远期利益打一些折扣。接近 1 的γ更关注长期的累计奖励,接近 0 的γ更考虑短期奖励。

回报的计算:在一个马尔可夫奖励过程中,从第t时刻状态St开始,直到终止状态时,所有奖励的衰减之和称为回报Gt(Return)。

计算公式:

其中,Rt表示在时刻t获得的奖励。对马尔可夫过程添加奖励函数,构建成一个马尔可夫奖励过程。例如,进入状态s2可以得到-2奖励,表明我们不希望进入s2。进入s4可以获得最高的奖励10,但是进入s6之后奖励为0,并且此时序列也终止了。

手动计算G1的奖励,选取s1为起始状态,设置γ=0.5,采样到一条状态序列为s1->s2->s3->s6,就可以计算s1的回报G1=-2.5,得到 G1 = -1 + 0.5*(-2) + 0.5^2*(-2) = -2.5。

状态价值计算

在马尔可夫奖励过程中,一个状态的期望回报(即从这个状态出发的未来累积奖励的期望)被称为这个状态的价值(value)。所有状态的价值就组成了价值函数(value function),价值函数的输入为某个状态,输出为这个状态的价值。

计算公式:V(s)为价值,Gt为某个状态的回报。

在上式的最后一个等号中,一方面,即时奖励的期望正是奖励函数的输出,另一方面,等式中剩余部分可以根据从状态出发的转移概率得到。获得马尔可夫奖励过程中非常有名的贝尔曼方程(Bellman equation)。

求解得到各个状态的价值V(s),

我们现在用贝尔曼方程来进行简单的验证。例如,对于状态s4来说,当γ=0.5时,则

可以发现左右两边的值是相等的,说明我们求解得到的价值函数是满足状态为时的贝尔曼方程。

参考文章:https://hrl.boyuai.com/chapter/1/马尔可夫决策过程/

2. 用R语言实现马尔可夫链

我们用R语言模拟上面的整个马尔可夫决策的计算过程,先使用手搓的方法,后面再掉包实现。

2.1 马尔可夫过程

首先,定义状态集合s,共6种状态 s1,s2,s3,s4,s5,s6。


# 6种状态
> s <- c("s1", "s2", "s3", "s4", "s5", "s6")
> s
[1] "s1" "s2" "s3" "s4" "s5" "s6"

定义转移概率矩阵p


> p <- matrix(c(
+   0.9,0.1,  0,  0,  0,  0,
+   0.5,  0,0.5,  0,  0,  0,
+     0,  0,  0,0.6,  0,0.4,
+     0,  0,  0,  0,0.3,0.7,
+     0,0.2,0.3,0.5,  0,  0,
+     0,  0,  0,  0,  0,  1
+ ), nrow = 6, byrow = TRUE, dimnames = list(s, s))

查看矩阵结果 
> p
    s1  s2  s3  s4  s5  s6
s1 0.9 0.1 0.0 0.0 0.0 0.0
s2 0.5 0.0 0.5 0.0 0.0 0.0
s3 0.0 0.0 0.0 0.6 0.0 0.4
s4 0.0 0.0 0.0 0.0 0.3 0.7
s5 0.0 0.2 0.3 0.5 0.0 0.0
s6 0.0 0.0 0.0 0.0 0.0 1.0

画图转移概率矩阵


> library(diagram)
> plotmat(p,lwd = 1, box.lwd = 1, cex.txt = 0.8, 
+         box.size = 0.02, box.type = "circle", shadow.size = 0.001,
+         box.col = "light blue",  arr.length=0.1, arr.width=0.1,
+         self.cex = 1.5,self.shifty = -0.01,self.shiftx = 0.04, 
+         main = "Markov Chain")

模拟随机的状态转移过程。


# 模拟马尔科夫链的函数
> simulate_markov_chain <- function(transition_matrix, initial_state, n_steps, stop=NULL) {
+   states <- rownames(transition_matrix)
+   chain <- character(n_steps + 1)
+   chain[1] <- initial_state
+   
+   for (i in 1:n_steps) {
+     current_state <- chain[i]
+     next_state <- sample(states, size = 1, prob = transition_matrix[current_state, ])
+     chain[i + 1] <- next_state
+   }
+   
+   # 停止状态
+   if(!is.null(stop)){
+     chain<-chain[1:which(chain==stop)[1]]  
+   }
+   
+   return(chain)
+ }

模拟马尔科夫链的结果。


# 设置初始状态和步数
> initial_state <- "s1" # 初始状态
> n_steps <- 300        # 执行步数
> stop_state<-'s6'      # 结束状态
> markov_chain <- simulate_markov_chain(p, initial_state, n_steps,stop_state)
> markov_chain
[1] "s1" "s1" "s1" "s1" "s1" "s2" "s3" "s6"

# 再次随机模拟结果
> markov_chain <- simulate_markov_chain(p, initial_state, n_steps)
> markov_chain
[1] "s1" "s1" "s1" "s2" "s3" "s4" "s5" "s4" "s6"

可视化展示马尔科夫链过程


> library(ggplot2)
> library(dplyr)
 
> # 准备数据用于绘图
> chain_df <- data.frame(
+     step = 1:length(markov_chain),
+     state = factor(markov_chain, levels = s)
+ )
 
> # 绘制马尔科夫链路径
> ggplot(chain_df, aes(x = step, y = state, group = 1)) +
+     geom_line(color = "gray") +
+     geom_point(aes(color = state), size = 3) +
+     theme_minimal()

模拟多条马尔科夫链观察收敛性


# 5条路径
> n_simulations <- 5
> n_steps <- 50
> simulations <- list()

> for (i in 1:n_simulations) {
+   simulations[[i]] <- simulate_markov_chain(p, initial_state, n_steps)
+ }

# 准备数据用于绘图
> sim_df <- data.frame(
+   step = rep(0:n_steps, n_simulations),
+   state = unlist(simulations),
+   simulation = rep(1:n_simulations, each = n_steps + 1)
+ )

# 绘制多条马尔科夫链
> ggplot(sim_df, aes(x = step, y = state, group = simulation, color = factor(simulation))) +
+   geom_line(alpha = 0.6) +
+   geom_point(size = 1.5) +
+   labs(title = "多条马尔科夫链模拟",
+        x = "时间步", y = "状态",
+        color = "模拟编号") +
+   theme_minimal()

我们模拟了5条路径的马尔科夫链

计算马尔科夫链的稳态分布


# 计算稳态分布
> calculate_steady_state <- function(transition_matrix, tolerance = 1e-6) {
+   n_states <- nrow(transition_matrix)
+   # 初始猜测
+   pi <- rep(1/n_states, n_states)
+   
+   while (TRUE) {
+     pi_new <- pi %*% transition_matrix
+     if (max(abs(pi_new - pi)) < tolerance) break
+     pi <- pi_new
+   }
+   
+   return(as.vector(pi_new))
+ }

> steady_state <- calculate_steady_state(p)
> names(steady_state) <- s

# 稳态分布:
> steady_state
          s1           s2           s3           s4           s5           s6 
1.663447e-05 1.794239e-06 1.016132e-06 7.652676e-07 2.406675e-07 9.999795e-01

# 可视化稳态分布
> barplot(steady_state, col = c("gold", "gray", "blue"),
+         main = "马尔科夫链稳态分布",
+         ylab = "概率", xlab = "状态",
+         ylim = c(0, 0.6))

2.2 马尔可夫奖励过程

奖励过程:为每个状态定义了即时奖励,模拟了带奖励的马尔科夫链,并计算了总奖励和折现奖励。

编写带奖励的马尔科夫链函数,用于奖励计算。


# 模拟带奖励的马尔科夫链
> simulate_markov_reward <- function(transition_matrix, rewards, initial_state, n_steps, gamma = 0.9, stop=NULL) {
+     states <- rownames(transition_matrix)
+     chain <- character(n_steps + 1)
+     reward_sequence <- numeric(n_steps + 1)
+     discounted_rewards <- numeric(n_steps + 1)
+     
+     chain[1] <- initial_state
+     reward_sequence[1] <- rewards[initial_state]
+     discounted_rewards[1] <- (gamma^0) * rewards[initial_state]
+     
+     for (i in 1:n_steps) {
+         current_state <- chain[i]
+         next_state <- sample(states, size = 1, prob = transition_matrix[current_state, ])
+         chain[i + 1] <- next_state
+         reward_sequence[i + 1] <- rewards[next_state]
+         discounted_rewards[i + 1] <- (gamma^i) * rewards[next_state]
+     }
+     
+     return(list(
+         chain = chain,
+         reward = unlist(reward_sequence),
+         discounted_reward = unlist(discounted_rewards),
+         total_reward = sum(unlist(reward_sequence)),
+         total_discounted_reward = sum(unlist(discounted_rewards))
+     ))
+ }

设置模拟参数


> initial_state <- "s1"    # 初始状态
> n_steps <- 100           # 步长
> gamma <- 0.9             # 折扣因子

# 奖励规则
> rewards<-data.frame("s1"=-1,"s2"=-2,"s3"=-2,"s4"=10,"s5"=1,"s6"=0)

运行带奖励的马尔科夫链函数,


> result <- simulate_markov_reward(p, rewards, initial_state, n_steps, gamma)
 
# 总奖励
> result$total_reward
[1] -23

# 总折现奖励
> result$total_discounted_reward
[1] -9.48585606660266

查看完整和运行结果


> result
$chain          # 马尔科夫链过程
  [1] "s1" "s1" "s1" "s1" "s1" "s1" "s1" "s1" "s1" "s1" "s1"
 [12] "s1" "s1" "s1" "s2" "s1" "s1" "s1" "s1" "s1" "s1" "s1"
 [23] "s1" "s2" "s1" "s1" "s1" "s2" "s3" "s4" "s6" "s6" "s6"
 [34] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
 [45] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
 [56] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
 [67] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
 [78] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
 [89] "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6" "s6"
[100] "s6" "s6"

$reward        # 各个步骤的奖励值
  [1] -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -2 -1 -1 -1
 [19] -1 -1 -1 -1 -1 -2 -1 -1 -1 -2 -2 10  0  0  0  0  0  0
 [37]  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 [55]  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 [73]  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
 [91]  0  0  0  0  0  0  0  0  0  0  0

$discounted_reward   # 各个步骤的折现奖励值
  [1] -1.00000000 -0.90000000 -0.81000000 -0.72900000
  [5] -0.65610000 -0.59049000 -0.53144100 -0.47829690
  [9] -0.43046721 -0.38742049 -0.34867844 -0.31381060
 [13] -0.28242954 -0.25418658 -0.45753585 -0.20589113
 [17] -0.18530202 -0.16677182 -0.15009464 -0.13508517
 [21] -0.12157665 -0.10941899 -0.09847709 -0.17725876
 [25] -0.07976644 -0.07178980 -0.06461082 -0.11629947
 [29] -0.10466953  0.47101287  0.00000000  0.00000000
 [33]  0.00000000  0.00000000  0.00000000  0.00000000
 [37]  0.00000000  0.00000000  0.00000000  0.00000000
 [41]  0.00000000  0.00000000  0.00000000  0.00000000
 [45]  0.00000000  0.00000000  0.00000000  0.00000000
 [49]  0.00000000  0.00000000  0.00000000  0.00000000
 [53]  0.00000000  0.00000000  0.00000000  0.00000000
 [57]  0.00000000  0.00000000  0.00000000  0.00000000
 [61]  0.00000000  0.00000000  0.00000000  0.00000000
 [65]  0.00000000  0.00000000  0.00000000  0.00000000
 [69]  0.00000000  0.00000000  0.00000000  0.00000000
 [73]  0.00000000  0.00000000  0.00000000  0.00000000
 [77]  0.00000000  0.00000000  0.00000000  0.00000000
 [81]  0.00000000  0.00000000  0.00000000  0.00000000
 [85]  0.00000000  0.00000000  0.00000000  0.00000000
 [89]  0.00000000  0.00000000  0.00000000  0.00000000
 [93]  0.00000000  0.00000000  0.00000000  0.00000000
 [97]  0.00000000  0.00000000  0.00000000  0.00000000
[101]  0.00000000

$total_reward   # 总奖励
[1] -23

$total_discounted_reward    # 总折现奖励
[1] -9.485856

可视化马尔科夫链状态序列,以上面带奖励的马尔科夫链生成的数据集result为基础。


> library(ggplot2)
> library(gridExtra)
 
> # 准备数据
> sim_data <- data.frame(
+     step = 0:n_steps,
+     state = factor(result$chain, levels = s),
+     reward = result$reward,
+     discounted_reward = result$discounted_reward
+ )

> # 绘制马尔科夫链状态序列状态
> p1 <- ggplot(sim_data, aes(x = step, y = state, color = state)) +
+     geom_point(size = 3) +
+     geom_line(aes(group = 1), color = "gray") +
+     labs(title = "马尔科夫链状态序列", x = "时间步", y = "状态")

# 绘制马尔科夫链状态序列奖励
> p2 <- ggplot(sim_data, aes(x = step, y = reward)) +
+     geom_col(aes(fill = state)) +
+     scale_fill_manual(values = c("Sunny" = "gold", "Cloudy" = "gray", "Rainy" = "blue")) +
+     labs(title = "即时奖励", x = "时间步", y = "奖励")

# 绘制马尔科夫链状态序列折现奖励 
> p3 <- ggplot(sim_data, aes(x = step, y = discounted_reward)) +
+     geom_col(aes(fill = state)) +
+     scale_fill_manual(values = c("Sunny" = "gold", "Cloudy" = "gray", "Rainy" = "blue")) +
+     labs(title = paste0("折现奖励 (γ=", gamma, ")"), x = "时间步", y = "折现奖励")
> 
> # 组合图形
> grid.arrange(p1, p2, p3, ncol = 1)

价值函数计算,通过解析解法,直接求解Bellman方程得到精确解。


# 计算状态价值函数
> calculate_state_values <- function(transition_matrix, rewards, gamma = 0.9, epsilon = 1e-6) {
+     n_states <- nrow(transition_matrix)
+     I <- diag(n_states)
+     
+     # 构建线性方程组: V = R + γPV → (I - γP)V = R
+     A <- I - gamma * transition_matrix
+     b <- rewards
+     
+     # 解线性方程组
+     V <- solve(A, b)
+     
+     return(V)
+ }

查看参数


> p
    s1  s2  s3  s4  s5  s6
s1 0.9 0.1 0.0 0.0 0.0 0.0
s2 0.5 0.0 0.5 0.0 0.0 0.0
s3 0.0 0.0 0.0 0.6 0.0 0.4
s4 0.0 0.0 0.0 0.0 0.3 0.7
s5 0.0 0.2 0.3 0.5 0.0 0.0
s6 0.0 0.0 0.0 0.0 0.0 1.0

> rewards
  s1 s2 s3 s4 s5 s6
1 -1 -2 -2 10  1  0

> gamma<-0.5
> gamma
[1] 0.5

计算状态价值


> state_values <- calculate_state_values(p, rewards, gamma)
> state_values
       s1        s2        s3        s4        s5        s6 
-2.019502 -2.214518  1.161428 10.538093  3.587286  0.000000 

本文初步探索了,马尔科夫决策过程中的基本概念,包括奖励过程、价值函数和策略优化。通过调整折扣因子γ、奖励值或转移矩阵来观察这些变化如何影响最优策略和价值函数。

本文的代码已上传到github:https://github.com/bsspirit/ml/blob/master/mavkov.r

本文有部分代码是通过Deepseek生成,再次惊叹Deepseek的强大。

转载请注明出处:
http://blog.fens.me/r-markov/

用R语言实现方差分析ANOVA

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹,分析师/程序员/Quant: R,Java,Nodejs
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-stat-anova/

前言

方差分析是一种基本统计学方法,在众多领域都有非常广泛的应用,比如评估不同药物的疗效,不同的广告策略的效果。

大模型的时代,是否能用方差分析,判断大模型结论与实际结论,是否有显著性差异,那么就能够快速验证大模型的是否产生了幻觉,以此来对大模型进行改进。

目录

  1. 方差分析介绍
  2. 用R语言方差分析aov()函数
  3. 用R语言方进行方差分析

1. 方差分析介绍

方差分析(Analysis of Variance,简称ANOVA)是一种用于比较多个群体均值是否存在显著差异的统计方法。它通过分析数据中不同来源的变异来检验均值差异的显著性。

方差分析的核心思想是将总变异分解为,组间变异(不同处理组之间的差异)和组内变异(同一组内的个体差异),通过比较这两种变异的相对大小来判断均值差异是否显著。

方差分析的主要类型为,

  • 单因素方差分析(One-way ANOVA):比较一个分类自变量(因素)对连续因变量的影响,例如:比较三种不同教学方法对学生成绩的影响
  • 双因素方差分析(Two-way ANOVA):同时考察两个分类自变量对连续因变量的影响,可以分析主效应和交互效应,例如:研究性别(男/女)和教学方法(A/B/C)对学生成绩的共同影响
  • 多元方差分析(MANOVA):适用于多个连续因变量的情况

ANOVA的核心目标是区分数据变异性的来源:是由实验条件(或处理)的不同引起的,还是由随机变异(即自然波动或实验误差)引起的。这种区分有助于我们判断实验条件是否对研究变量有显著影响。

ANOVA的关键组成部分

  • 总方差(Total Variance): 数据总体的方差,包括组间方差和组内方差。
  • 组间方差(Between-Group Variance): 不同组(或处理条件)间均值的差异。
  • 组内方差(Within-Group Variance): 同一组内个体间的差异。

通过比较组间方差和组内方差,ANOVA帮助我们判断各组间是否存在显著的均值差异。如果组间方差显著大于组内方差,我们有理由相信不同组的均值存在显著差异。

方差分析的基本假设

  • 正态性:每个组的数据应近似呈正态分布。这意味着数据的分布应该是对称的,没有明显的偏斜,使用Shapiro-Wilk检验
  • 方差齐性:所有组的方差应该大致相等。这个假设确保了不同组别的数据具有一致的波动性,使用Levene’s Test或Bartlett’s Test来检验
  • 独立性:数据点之间应该是相互独立的,即一个数据点的值不应影响或决定另一个数据点的值。

方差分析步骤

  1. 提出假设:零假设(H₀):所有组的均值相等,即组间不存在显著差异。备择假设(H₁):至少有两组的均值不等,即存在至少一个组间的显著差异。
  2. 计算组间和组内方差:组间方差(Between-Group Variance):计算每个组的均值与总体均值之间的差异,反映了不同处理或条件下数据的变化程度。组内方差(Within-Group Variance):计算组内数据点与各自组均值的差异,表示在相同条件下的数据波动。
  3. 计算F统计量:F值是方差分析中的核心统计量,它是组间方差与组内方差的比率:F = 组间方差 / 组内方差 ,较高的F值通常表明组间存在显著差异。但我们需要通过F分布来确定这个差异是否统计上显著。
  4. 将计算得到的F值与临界值比较,或直接看p值,根据自由度和显著性水平(通常是0.05)找到F值的临界值。如果计算出的F值超过临界值,我们拒绝零假设,认为至少有两组间存在显著差异。
  5. 做出统计决策

事后检验:当方差分析结果显示显著差异时,需要进行事后检验(Post-hoc tests)来确定具体哪些组之间存在差异,常用方法包括:Tukey’s HSD检验,Bonferroni校正,Scheffe检验。

2. 用R语言方差分析aov()函数

R中的方差分析(ANOVA)是一种统计方法,用于比较两个或多个组之间的均值是否存在显著差异。在R语言中,可以使用ANOVA函数(aov)进行方差分析。

在R语言中,自带的stat包的aov()函数,就是我们最常用方差分析的计算函数。

aov()函数的语法为aov(formula, data=dataframe),y是因变量,字母ABC代表因子。

R语言代码的写法:


model <- aov(response ~ group, data = dataset)
summary(model)
符号用途
~分隔符号,左边为响应变量(因变量),右边为解释变量(自变量),如 y ~ A + B + C
:表示预测变量的交互项,A与B的交互,如y ~ A + B + A:B
*表示所有可能交互项的简洁方式,如 y ~ A*B*C 等同于 y ~ A+B+C + A:B + A:C + B:C + A:B:C
^表示交互项达到某个次数,如y~(A+B+C)^2 等同于 y~ A+B+C + A:B + A:C + B:C
.表示包含除因变量外的所有变量,如y~. 等同于 y~ A+B+C

下面是常见研究设计的表达式

设计表达式
单因素ANOVAy ~ A
含单个协变量的单因素ANOVAy ~ x + A
双因素ANOVAy ~ A * B
含两个协变量的双因素ANOVAy ~ x1 + x2 + A * B
随机化区组y ~ B + A (B是区组因子)
单因素组内ANOVAy ~ A + Error(subject/A)
含单个组内因子(W)和单个组间因子的重复测量ANOVAy ~ B * W + Error(Subject/W)

3. R语言实现方差分析

数据集使用datarium包的职位满意度调查jobsatisfaction数据集。


# 安装datarium
> install.packages("datarium")

# 加载程序包
> library(datarium)
> library(dplyr)

查看jobsatisfaction数据集的基本情况,共4列,ID,gender性别,education_level学历水平,score得分。


> data("jobsatisfaction", package = "datarium")
> jobsatisfaction
# A tibble: 58 × 4
   id    gender education_level score
                 
 1 1     male   school           5.51
 2 2     male   school           5.65
 3 3     male   school           5.07
 4 4     male   school           5.51
 5 5     male   school           5.94
 6 6     male   school           5.8 
 7 7     male   school           5.22
 8 8     male   school           5.36
 9 9     male   school           4.78
10 10    male   college          6.01
# ℹ 48 more rows
# ℹ Use `print(n = ...)` to see more rows

# 查看统计概率
> summary(jobsatisfaction)
       id        gender     education_level     score       
 1      : 1   male  :28   school    :19     Min.   : 4.780  
 2      : 1   female:30   college   :19     1st Qu.: 5.800  
 3      : 1               university:20     Median : 6.380  
 4      : 1                                 Mean   : 6.963  
 5      : 1                                 3rd Qu.: 8.515  
 6      : 1                                 Max.   :10.000  

对数据集进行分组gender, education_level,计算均值和标准差


> jobsatisfaction %>%
+   group_by(gender, education_level) %>%
+   get_summary_stats(score, type = "mean_sd")
# A tibble: 6 × 6
  gender education_level variable     n  mean    sd
                     
1 male   school          score        9  5.43 0.364
2 male   college         score        9  6.22 0.34 
3 male   university      score       10  9.29 0.445
4 female school          score       10  5.74 0.474
5 female college         score       10  6.46 0.475
6 female university      score       10  8.41 0.938

画出分组的数据分箱图


> ggboxplot(
+   jobsatisfaction, x = "gender", y = "score",
+   color = "education_level", palette = "jco"
+ )

建立方差分析模型,有几个变量我们就全部都加入分析,以score为因变量,以gender和education_level的组合为自变量。


# 建模
> model<- aov(score ~ gender*education_level, data = jobsatisfaction)

# 查看因子显著性水平
> summary(model)
                       Df Sum Sq Mean Sq F value  Pr(>F)    
gender                  1   0.54    0.54   1.787 0.18709    
education_level         2 113.68   56.84 187.892 < 2e-16 ***
gender:education_level  2   4.44    2.22   7.338 0.00156 ** 
Residuals              52  15.73    0.30                    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

其中,education_level和gender:education_level的P值对score有影响,gender没有影响。

对模型进行正态分布检验shapiro.test,p值大于0.05,说明符合正态分布。


> shapiro.test(residuals(model))

	Shapiro-Wilk normality test

data:  residuals(model)
W = 0.96786, p-value = 0.1267

进行方差齐性检验bartlett.test,p值小于0.05,说明education_level方差有显著地不同,方差不是齐性的。


> bartlett.test(score ~ education_level, data = jobsatisfaction)

	Bartlett test of homogeneity of variances

data:  score by education_level
Bartlett's K-squared = 11.651, df = 2, p-value = 0.002951

进行方差齐性检验bartlett.test,p值大于0.05,说明gender方差没有显著地不同,方差是齐性的。


> bartlett.test(score ~ gender, data = jobsatisfaction)

	Bartlett test of homogeneity of variances

data:  score by gender
Bartlett's K-squared = 2.3701, df = 1, p-value = 0.1237

Bartlett's K-squared:Bartlett's K-squared是Bartlett检验的统计量,用于衡量不同组间方差的差异。它是通过计算各组的方差并比较它们之间的差异而得出的。如果各组方差相等,则K-squared值应较小;如果方差不相等,K-squared值则较大。

df(自由度):自由度(degrees offreedom,df)是统计分析中的一个重要概念,它指的是在计算某个统计量时,可以自由变化的数值的数量。在上面 Bartlett检验中,由于假设有三个不同种族的组来比较它们之间的出生体重(bwt)的方差,自由度等于组数减一(这里为2)。

p-value(p值):p值用于衡量观察到的统计量在假设检验中的极端性。具体来说,它表示在零假设为真的情况下,观察到像样或更极端结果的概率。若p值小于某个显著性水平(通常为0.05),则我们拒绝零假设;若p值大于显著性水平,则不能拒绝零假设。在Bartlett检验中,p值大于0.05表示没有足够的证据表明各组间方差存在显著差异。

使用Tukey’s HSD检验,进行分组的显著差异。


> TukeyHSD(model)
  Tukey multiple comparisons of means
    95% family-wise confidence level

Fit: aov(formula = score ~ gender * education_level, data = jobsatisfaction)

$gender
                  diff        lwr        upr     p adj
female-male -0.1932143 -0.4832329 0.09680436 0.1870895

$education_level
                        diff       lwr      upr     p adj
college-school     0.7573684 0.3268386 1.187898 0.0002637
university-school  3.2518102 2.8266960 3.676924 0.0000000
university-college 2.4944417 2.0693275 2.919556 0.0000000

$`gender:education_level`
                                        diff          lwr        upr
female:school-male:school          0.3143333 -0.433358082  1.0620247
male:college-male:school           0.7966667  0.029551460  1.5637819
female:college-male:school         1.0363333  0.288641918  1.7840247
male:university-male:school        3.8653333  3.117641918  4.6130247
female:university-male:school      2.9793333  2.231641918  3.7270247
male:college-female:school         0.4823333 -0.265358082  1.2300247
female:college-female:school       0.7220000 -0.005749384  1.4497494
male:university-female:school      3.5510000  2.823250616  4.2787494
female:university-female:school    2.6650000  1.937250616  3.3927494
female:college-male:college        0.2396667 -0.508024749  0.9873581
male:university-male:college       3.0686667  2.320975251  3.8163581
female:university-male:college     2.1826667  1.434975251  2.9303581
male:university-female:college     2.8290000  2.101250616  3.5567494
female:university-female:college   1.9430000  1.215250616  2.6707494
female:university-male:university -0.8860000 -1.613749384 -0.1582506
                                      p adj
female:school-male:school         0.8132166
male:college-male:school          0.0374890
female:college-male:school        0.0019203
male:university-male:school       0.0000000
female:university-male:school     0.0000000
male:college-female:school        0.4086560
female:college-female:school      0.0529764
male:university-female:school     0.0000000
female:university-female:school   0.0000000
female:college-male:college       0.9317495
male:university-male:college      0.0000000
female:university-male:college    0.0000000
male:university-female:college    0.0000000
female:university-female:college  0.0000000
female:university-male:university 0.0087499

TukeyHSD的检验,education_level的显著性水平最高,所以我们画出education_level检验图。


> plot(TukeyHSD(model, "education_level"))

本文通过方差分析,实现对不同变量的影响权重的计算,可以通过统计学的方法,发现新的变量特征。

转载请注明出处:
http://blog.fens.me/r-stat-anova/

用R语言实现KDTree多维索引

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹,分析师/程序员/Quant: R,Java,Nodejs
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-kdtree/

前言

多维向量数据检索,在自然语言处理以及标签向量匹配等应用场景,越来越多的被使用到。提升匹配效率,就能使用体验有巨大的改善。本文从kd树的底层数据结构,和R语言算法实现的角度,介绍kdtree在多维度数据匹配过程中的应用。

目录

  1. KD Tree算法介绍
  2. 用R语言实现KD Tree
  3. 进行三维索引匹配

1. KD Tree算法介绍

k-D Tree(KDT , k-Dimension Tree) 是一种可以对K 维资料进行划分的资料结构,可以看成二元搜索树的一种延伸,不断的对空间中的维度做划分,利用搜索树剪枝的特性缩短时间复杂度,主要应用在多维空间搜寻,例如最近邻居搜索。在结点数 n 远大于 2^k 时,应用 k-D Tree 的时间效率很好。

k-D Tree 具有二叉搜索树的形态,二叉搜索树上的每个结点都对应 k 维空间内的一个点。其每个子树中的点都在一个 k 维的超长方体内,这个超长方体内的所有点也都在这个子树中。

假设我们已经知道了 k 维空间内的 n 个不同的点的坐标,要将其构建成一棵 k-D Tree,步骤如下:

  1. 若当前超长方体中只有一个点,返回这个点。
  2. 选择一个维度,将当前超长方体按照这个维度分成两个超长方体。
  3. 选择切割点:在选择的维度上选择一个点,这一维度上的值小于这个点的归入一个超长方体(左子树),其余的归入另一个超长方体(右子树)。
  4. 将选择的点作为这棵子树的根节点,递归对分出的两个超长方体构建左右子树,维护子树的信息。

以k=2时举例

其构建出 k-D Tree 的形态可能是这样的:

其中树上每个结点上的坐标是选择的分割点的坐标,非叶子结点旁的 x 或 y 是选择的切割维度。

这样的复杂度无法保证。对于 2,3 两步,我们提出两个优化:

  1. 轮流选择 k 个维度,以保证在任意连续 k 层里每个维度都被切割到。
  2. 每次在维度上选择切割点时选择该维度上的 中位数,这样可以保证每次分成的左右子树大小尽量相等。
    可以发现,使用优化 2 后,构建出的 k-D Tree 的树高最多为 log(n)+O(1)。

现在,构建 k-D Tree 时间复杂度的瓶颈在于快速选出一个维度上的中位数,并将在该维度上的值小于该中位数的置于中位数的左边,其余置于右边。如果每次都使用 sort 函数对该维度进行排序,时间复杂度是 O(nlog^2(n)) 的。事实上,单次找出 n 个元素中的中位数并将中位数置于排序后正确的位置的复杂度可以达到 O(n)。

我们来回顾一下快速排序的思想。每次我们选出一个数,将小于该数的置于该数的左边,大于该数的置于该数的右边,保证该数在排好序后正确的位置上,然后递归排序左侧和右侧的值。这样的期望复杂度是 O(nlog(n)) 的。但是由于 k-D Tree 只要求要中位数在排序后正确的位置上,所以我们只需要递归排序包含中位数的 一侧。可以证明,这样的期望复杂度是 O(n) 的。借助这种思想,构建 k-D Tree 时间复杂度是 O(nlog(n)) 的。

参考文章:https://oi-wiki.org/ds/kdt/

2. 用R语言实现KD Tree

首先,安装less(Learning with Subset Stacking)包,less包中包含了多种的算法,DKTree就是集成的一种算法。


# 安装
> install.packages("less")

# 加载
> library(less)

选定数据集


# 选定数据集,以iris数据集为例,取前2例
> dat<-iris[,1:2]

# 查看数据内容
> head(dat)
  Sepal.Length Sepal.Width
1          5.1         3.5
2          4.9         3.0
3          4.7         3.2
4          4.6         3.1
5          5.0         3.6
6          5.4         3.9

创建一个kdtree实例化对象kdt,kdt对象中一共3个函数,分别是clone,initialize,query。


# 创建
> kdt <- KDTree$new(dat)

# 查看数据结构
> kdt

  Public:
    clone: function (deep = FALSE) 
    initialize: function (X = NULL) 
    query: function (query_X = private$X, k = 1) 
  Private:
    X: data.frame

接下来,我们生成一些与创建 kdtree 不同的查询点,用于查询。


> q_data <- iris[1:5,1:2] + array(rnorm(10)*0.1, dim=c(5,2))
> q_data
  Sepal.Length Sepal.Width
1     5.067010    3.464073
2     4.913018    3.100844
3     4.678508    3.203468
4     4.634521    3.105371
5     4.948339    3.594034

把q_data输入kdt对象,用于kdtree查询。


# 查询k=2,为二维
> res<-kdt$query(query_X = q_data, k = 2)
> res
$nn.idx                      # 输出索引号
     [,1] [,2]
[1,]   18    1
[2,]   10   35
[3,]   30    3
[4,]    4   48
[5,]   38    5
                   
$nn.dists                     # 输出距离
           [,1]       [,2]
[1,] 0.04877580 0.04877580
[2,] 0.01304518 0.01304518
[3,] 0.02177005 0.02177005
[4,] 0.03493634 0.10072915
[5,] 0.04870594 0.05200411

通过索引号,还原的dat的原始数据中


> src<-matrix(col1=dat[res$nn.idx[,1],1],col2=dat[res$nn.idx[,2],2])
> src
  col1 col2
1  5.1  3.5
2  4.9  3.1
3  4.7  3.2
4  4.6  3.2
5  4.9  3.6

3. 进行三维索引匹配

接下来,我们再尝试一下三维索引的匹配。


> # 三维数据集
> dat<-iris[,1:3]
> head(dat)
  Sepal.Length Sepal.Width Petal.Length
1          5.1         3.5          1.4
2          4.9         3.0          1.4
3          4.7         3.2          1.3
4          4.6         3.1          1.5
5          5.0         3.6          1.4
6          5.4         3.9          1.7

# 建树
> kdt <- KDTree$new(dat)

# 生成三维测试数据
> q_data <- iris[1:5,1:3] + array(rnorm(10)*0.3, dim=c(5,2))
> q_data
  Sepal.Length Sepal.Width Petal.Length
1     5.193700    3.404217     1.493700
2     5.078012    3.019615     1.578012
3     4.914997    3.498524     1.514997
4     4.593170    2.982210     1.493170
5     5.094704    3.149830     1.494704

# 获得匹配结果
> res<-kdt$query(query_X = q_data, k = 3)
> res
$nn.idx
     [,1] [,2] [,3]
[1,]   40   29   28
[2,]   26   35   10
[3,]   44    8   38
[4,]    4   46   13
[5,]   10   35   50

$nn.dists
           [,1]       [,2]       [,3]
[1,] 0.09400598 0.09400598 0.09619658
[2,] 0.08339090 0.21032310 0.21032310
[3,] 0.12022173 0.13098663 0.15409891
[4,] 0.11818578 0.22754310 0.22754310
[5,] 0.20104879 0.20104879 0.20121780

通过KDTree的,能快速的让我们从多维数据中,找到最紧邻的点,进行数据搜索。

本文的代码已上传到github:https://github.com/bsspirit/r-string-match/blob/main/kdtree.r

转载请注明出处:
http://blog.fens.me/r-kdtree/

用R语言实现ngram算法

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹,分析师/程序员/Quant: R,Java,Nodejs
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-text-ngram/

前言

大语言模型的文字表达能力,让我们所惊叹算法的魅力。语言能力并不是AI与生俱来的,而且通过底层的各种算法像拼接积木似的组装出来的。我们需要有一个高质量的预料库,然后通过模型训练,你也能拥有文字表达能力。

ngram就给了我们,让惊叹变成现实的这种能力。

目录

  1. ngram算法介绍
  2. 用R语言实现ngram
  3. 生成式算法:用ngram生成一段文字

1. ngram算法介绍

n-gram是一种基于统计语言模型的算法,它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。N就是将句子做切割,N个单词为一组。每一个字节片段称为gram,对所有gram的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键gram列表,也就是这个文本的向量特征空间,列表中的每一种gram就是一个特征向量维度。

如:A B A C A B B,可以切分为


P(A B A C A B B) = P(A)*P(B|A)*P(A|A,B)*P(C|A,B,A)…*P(B|A B A C A B)

为了简化问题,我们可以假设当前的字出现只跟前一个字有关,这样就形成了二元的模型。


P(A B A C A B B) = P(A)*P(B|A)*P(A|B)*…*P(B|B)

ngram模型基于这样一种假设,第N个词的出现只与前面N-1个词相关,而与其它任何词都不相关,整句的概率就是各个词出现概率的乘积。这些概率可以通过直接从语料中统计N个词同时出现的次数得到。常用的,一元为unigram,是二元的bi-gram,三元的tri-gram。

  • 一元表示:P(A B A C A B B) = P(A)*P(B)*P(A)*P(C)*P(A)*P(B)*P(B)
  • 二元表示:P(A B A C A B B) = P(A)*P(B|A)*P(A|A)*P(C|A)…*P(B|B)
  • 三元表示:P(A B A C A B B) = P(A)*P(B|A)*P(A|A,B)*P(C|B,A)…*P(B|A,B)

当N值增大时:对单词的约束性更高,具有更高的辨识力,复杂度也随之增大。当N值减小时:词文本出现的次数概率更可靠,统计结果更可靠,但对词文本的约束性减小

为了考虑字词在开头和结尾的位置,可以在文本中加入 start 的 token 和 end 的 token。


P(A B A C A B B) = P(A|<start>)*P(B|A)*P(A|B)*…*P(B|B)*P(<end>|B)

N-gram 的使用方法:

  1. 文本预处理:
    首先,对原始文本进行分词、去除停用词、词干提取等预处理操作,以便得到适合进行N-gram 统计的词序列。
  2. 生成N-grams:
    根据设定的N 值,将预处理后的词序列拆分成若干个N-gram。
  3. 统计频率:
    统计每个N-gram 在文本中出现的次数,并计算其频率。
  4. 构建语言模型:
    利用统计得到的N-gram 频率信息,构建一个语言模型,用于预测下一个词的概率。
  5. 使用语言模型:
    在实际应用中,可以使用构建好的语言模型来预测下一个词的出现概率,从而实现诸如文本生成、语音识别、机器翻译等任务。

2. 用R语言实现ngram

ngram是一个 R 包,用于构建 n-gram(即“分词”),以及根据给定文本输入的 n-gram 结构生成新文本(即“胡言乱语”)。该包可用于进行严肃的分析,或创建说些有趣话语的“机器人”。ngram包旨在以极快的速度对已标记的语料库进行标记化、汇总和汇总。由于其架构设计,我们还能够处理海量文本,并且性能扩展性非常好。基准测试和示例用法可在软件包简介中找到。

ngram包安装和加载


# 安装
> install.packages("ngram")

# 加载
> library(ngram)

创建文本变量x


# 创建变量
> x <- "A B A C A B B"

# 查看字符串属性
> string.summary(x)
Chars:       13
Letters:     7
Whitespace:  6
Punctuation: 0
Digits:      0
Words:       7
Sentences:   0
Lines:       1 
Wordlens:    0 7 
             9 1 
Senlens:     0 
             10 
Syllens:     0 3 
             9 1 

进行ngram二元分词n=2,并预测下一个出现的词。


# ngram二元分词
> ng <- ngram (x , n =2)
> ng
An ngram object with 5 2-grams 

查看ngram的分词预测,可以设置output=”full”时是打印所有的结果。使用output=”truncated”,只会显示前 5 个 n-gram。


# 查看分词结果
> print (ng , output ="full")
C A | 1 
B {1} | 

B A | 1 
C {1} | 

B B | 1 
NULL {1} | 

A C | 1 
A {1} | 

A B | 2 
B {1} | A {1} | 

我们可以看到每个 3-gram 及其下一个可能的 “词”,以及每个词在给定 n-gram 之后的出现频率。

  • 第一组C A 的下一个可能词是 B,因为在输入的字符串中,序列 C A 的后面只有 B。
  • 第五组A B 的后面有一次 A 和一次 B。
  • 第三组B B 是终结符,即后面什么也没有。

生成短语表,文本的 ngram 表示一旦生成,就可以非常简单地获取一些有趣的摘要信息。函数 get.phrasetable() 生成了一个 n-grams 表,以及它们在文本中的频率和比例。


> get.phrasetable ( ng )
  ngrams freq      prop
1   A B     2 0.3333333
2   C A     1 0.1666667
3   B A     1 0.1666667
4   B B     1 0.1666667
5   A C     1 0.1666667

AB,出现过2次,概率是0.33。其他组合,都只出现过一次,概率是0.16.

再分别生成n=1和n=3的短语表。


> get.phrasetable ( ngram (x , n =1) )
  ngrams freq      prop
1     A     3 0.4285714
2     B     3 0.4285714
3     C     1 0.1428571

> get.phrasetable ( ngram (x , n =3) )
  ngrams freq prop
1 C A B     1  0.2
2 A B A     1  0.2
3 A B B     1  0.2
4 B A C     1  0.2
5 A C A     1  0.2

另外还有2个工具函数


# 提取组合
> get.ngrams(ng)
[1] "C A" "B A" "B B" "A C" "A B"

# 查看原始的数据
> get.string(ng)
[1] "A B A C A B B"

使用babble()函数,通过ngrams来生成与输入字符串具有相同统计属性的新字符串。

随机生成3组字符。


> babble (ng , 10)
[1] "A C A B B B B A B B "

> babble (ng , 10)
[1] "C A B B C A B B A C "

> babble (ng , 20)
[1] "B A C A B B B A C A B B A B B B A C A B "

我们也可以指定种子生成相同的内容。


> babble (ng , 10 , seed =10)
[1] "A C A B B B B A B B "

> babble (ng , 10 , seed =10)
[1] "A C A B B B B A B B "

3. 生成式算法:用ngram生成一段文字

通过babble(),是不是看到生成式大模型的影子了,为什么模型能生成出来说法通顺的文字呢。

我们多给一些语言描述,让ngram进行算法识别。

生成语言文字。


x<-c(
+ "how are you",
+ "I like you",
+ "I love you",
+ "I think you're the best for me",
+ "We re good friends",
+ "We're all good"
+ )

# 拼接成一个句子
> x<-str_c(x,collapse = " ")
> x
[1] "how are you I like you I love you I think you're the best for me We re good friends We're all good"

构建ngram模型


> ng <- ngram (x , n=2)
> ng
An ngram object with 20 2-grams 

# 查看语言序列
> print (ng , output ="truncated")
the best | 1 
for {1} | 

for me | 1 
We {1} | 

friends We're | 1 
all {1} | 

are you | 1 
I {1} | 

I think | 1 
you're {1} | 

[[ ... results truncated ... ]]

查看短语概率表


> get.phrasetable ( ng )
           ngrams freq       prop
1          you I     3 0.13636364
2       the best     1 0.04545455
3         for me     1 0.04545455
4  friends We're     1 0.04545455
5        are you     1 0.04545455
6        I think     1 0.04545455
7   good friends     1 0.04545455
8         I like     1 0.04545455
9   think you're     1 0.04545455
10      like you     1 0.04545455
11      all good     1 0.04545455
12        I love     1 0.04545455
13         me We     1 0.04545455
14      love you     1 0.04545455
15         We re     1 0.04545455
16       how are     1 0.04545455
17       re good     1 0.04545455
18    you're the     1 0.04545455
19      best for     1 0.04545455
20     We're all     1 0.04545455

接下来就是神奇的时刻,自动生成10个短语。


> babble (ng , 10)
[1] "me We re good friends We're all good I love "

> babble (ng , 10)
[1] "for me We re good friends We're all good I "

> babble (ng , 10)
[1] "you're the best for me We re good friends We're "

> babble (ng , 10)
[1] "think you're the best for me We re good friends "

> babble (ng , 10)
[1] "We're all good I think you're the best for me "

> babble (ng , 10)
[1] "like you I like you I think you're the best "

看上去,很符合预期。虽然比不是现在的大语言模型,能生成完整的语句,但是已经有了生成式模型的影子了。

本文的代码已上传到github:https://github.com/bsspirit/r-string-match/blob/main/ngram.r

转载请注明出处:
http://blog.fens.me/r-text-ngram/

用R语言计算文本的TF-IDF

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹,分析师/程序员/Quant: R,Java,Nodejs
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-text-tf-idf/

前言

在互联网的今天,我们每天都会生产和消费大量的文本信息,如报告、文档、新闻、聊天、图书、小说、语音转化的文字等。海量的文本信息,不仅提供扩宽的研究对象和研究领域,也为商业使用带来了巨大的机会。

TF-IDF是一种通过计算词频,进行文本信息检索的一种方式,常用对文本数据进行向量化特征的描述。

目录

  1. 什么是TF-IDF介绍
  2. tidytext包计算TF-IDF
  3. quanteda包计算TF-IDF

1. 什么是TF-IDF介绍

TF-IDF (词频-逆文档频率) 是一种在信息检索和文本挖掘中常用的加权技术。它衡量一个词语对于一个文档的重要性,通过结合词频(TF) 和逆文档频率(IDF) 来计算。

TF (词频): 表示词语在特定文档中出现的次数。一个词语在文档中出现的次数越多,通常认为它在该文档中越重要。
IDF (逆文档频率): 表示词语在整个语料库中的普遍程度。如果一个词语在很多文档中都出现,说明它比较常见,对区分文档的贡献就越小;反之,如果一个词语只出现在少数文档中,说明它对区分这些文档越重要。
TF-IDF 的计算: TF-IDF 值是TF 值和IDF 值的乘积。

公式:


TF (词频) = (词语在文档中出现的次数) / (文档中的总词数)
IDF (逆文档频率) = log(总文档数/ 包含该词语的文档数+ 1) (加1 是为了避免除以零的情况)
TF-IDF = TF * IDF


作用: TF-IDF 通过综合考虑词语在文档内的频率和在语料库中的普遍程度,能够有效地识别出对文档具有区分度的关键词。在搜索引擎中,TF-IDF 常被用于计算文档与用户查询的相关性,从而对搜索结果进行排序。

总结: TF-IDF 是一种简单而有效的文本特征提取方法,通过对词语的权重计算,可以帮助我们更好地理解和利用文本数据.

2. tidytext包计算TF-IDF

R语言中,有多个包都支持TF-IDF的计算。首先,我们先使用tidytext包。

安装tidytext包


# 安装tidytext包
> install.packages("tidytext")

# 加载tidytext包
> library(tidytext)

# 加载其他工具包
> library(dplyr)
> library(tibble)

定义10段文本数据,并进行编号。


# 定义文本数据
> ss <- c(
+     "R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大", 
+     "R语言作为统计学一门语言,一直在小众领域闪耀着光芒。", 
+     "现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。",
+     "直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。",
+     "随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。",
+     "TF-IDF (词频-逆文档频率) 是一种在信息检索和文本挖掘中常用的加权技术。",
+     "它衡量一个词语对于一个文档的重要性,通过结合词频(TF) 和逆文档频率(IDF) 来计算。",
+     "TF (词频): 表示词语在特定文档中出现的次数。一个词语在文档中出现的次数越多,通常认为它在该文档中越重要。",
+     "IDF (逆文档频率): 表示词语在整个语料库中的普遍程度。如果一个词语在很多文档中都出现,说明它比较常见,对区分文档的贡献就越小;反之,如果一个词语只出现在少数文档中,说明它对区分这些文档越重要。",
+     "TF-IDF 的计算: TF-IDF 值是TF 值和IDF 值的乘积。"
+ ) 

# 转成tibble类型,并增加列编号
> s1<- ss %>% as_tibble() %>% rowid_to_column("ID")

# 查看s1变量
> s1
# A tibble: 10 × 2
      ID value                                    
   <int> <chr>                                    
 1     1 R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的…
 2     2 R语言作为统计学一门语言,一直在小众领域闪耀着光芒。……
 3     3 现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。……
 4     4 直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。……
 5     5 随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。……
 6     6 TF-IDF (词频-逆文档频率) 是一种在信息检索和文本挖掘中常用的加权技术。…
 7     7 它衡量一个词语对于一个文档的重要性,通过结合词频(TF) 和逆文档频率(IDF)…
 8     8 TF (词频): 表示词语在特定文档中出现的次数。一个词语在文档中出现的次数越多…
 9     9 IDF (逆文档频率): 表示词语在整个语料库中的普遍程度。如果一个词语在很多文…
10    10 TF-IDF 的计算: TF-IDF 值是TF 值和IDF 值的乘积。……

计算每个文档中的词频,用于计算tf-idf


# 计算文档中的词频
> ss_words <- s1 %>% 
+   unnest_tokens(words, value) %>% 
+   count(ID, words, sort = TRUE);ss_words
# A tibble: 191 × 3
      ID words     n
   <int> <chr> <int>
 1     1 的        5
 2     9 文        5
 3     9 档        5
 4     1 r         3
 5     4 的        3
 6     5 的        3
 7     8 在        3
 8     8 文        3
 9     8 档        3
10     9 在        3
# 181 more rows
# Use `print(n = ...)` to see more rows

使用bind_tf_idf()函数,计算每个词的tf,idf,tf-idf。

tf-idf 的思想是通过减少常用词的权重并增加在文档集合或语料库中不经常使用的词的权重,在这种情况下,我们找到每个文档内容的重要词。计算 tf-idf 会尝试查找文本中重要(即常见)但不太常见的单词。


# 计算tf-idf
> ss_tfidf <- ss_words %>% 
+   bind_tf_idf(ID, words, n)
> ss_tfidf
# A tibble: 191 × 6
      ID words      n    tf   idf tf_idf
   <int> <chr>  <int> <dbl> <dbl>  <dbl>
 1     1 的        5 0.278  1.68  0.466
 2     9 文        5 0.455  1.29  0.585
 3     9 档        5 0.455  1.29  0.585
 4     1 r         3 0.429  1.68  0.720
 5     4 的        3 0.167  2.13  0.354
 6     5 的        3 0.167  2.19  0.365
 7     8 在        3 0.375  1.85  0.695
 8     8 文        3 0.273  1.85  0.505
 9     8 档        3 0.273  1.85  0.505
10     9 在        3 0.375  1.29  0.483
# 181 more rows
# Use `print(n = ...)` to see more rows

# 以tf_idf列分值高倒序排序
> ss_tfidf %>%
+   arrange(desc(tf_idf))
# A tibble: 191 × 6
      ID words      n    tf   idf tf_idf
   <int> <chr>  <int> <dbl> <dbl>  <dbl>
 1    10 值         3     1  2.70   2.70
 2    10 乘         1     1  2.70   2.70
 3    10 积         1     1  2.70   2.70
 4     2 一         1     1  2.41   2.41
 5     2 一直在     1     1  2.41   2.41
 6     2 作为       1     1  2.41   2.41
 7     2 光芒       1     1  2.41   2.41
 8     2 学         1     1  2.41   2.41
 9     2 小众       1     1  2.41   2.41
10     2 闪耀着     1     1  2.41   2.41
# 181 more rows
# Use `print(n = ...)` to see more rows

通过tf-idf判断2个文本的相似性


# 加载widyr包
> library(widyr)

# 判断2段文本的相似性
> ss_tfidf %>% 
+   pairwise_similarity(ID, words, tf_idf, sort = TRUE)
# A tibble: 70 × 3
   item1 item2 similarity
   <int> <int>      <dbl>
 1     8     9     0.135 
 2     9     8     0.135 
 3     7    10     0.0907
 4    10     7     0.0907
 5     6    10     0.0769
 6    10     6     0.0769
 7     3     2     0.0620
 8     2     3     0.0620
 9     7     9     0.0596
10     9     7     0.0596
# 60 more rows
# Use `print(n = ...)` to see more rows

程序提示第8段和第9段文本最相似,查看第8段和第9段文本。


> ss[8]
[1] "TF (词频): 表示词语在特定文档中出现的次数。一个词语在文档中出现的次数越多,通常认为它在该文档中越重要。"

> ss[9]
[1] "IDF (逆文档频率): 表示词语在整个语料库中的普遍程度。如果一个词语在很多文档中都出现,说明它比较常见,对区分文档的贡献就越小;反之,如果一个词语只出现在少数文档中,说明它对区分这些文档越重要。"

3. quanteda包计算TF-IDF

我们再换另一个包quanteda来计算TF-IDF。quanteda包,是一个能力非常强大的文本处理包,详细内容请参考文章用R语言进行量化文本分析quanteda

安装和加载quanteda


# 安装
> install.pacakges("quanteda")
> install.pacakges("quanteda.textstats")

# 加载
> library("quanteda")
> library("quanteda.textstats")

使用上文中已建立的ss文档对象,提取关键词,形成词频矩阵。


> ssdfm
Document-feature matrix of: 10 documents, 144 features (84.51% sparse) and 0 docvars.
       features
docs    r 的 极 客 理想 系列 文章 , 涵 盖了
  text1 3  5  1  1    1    1    1  5  1    1
  text2 1  0  0  0    0    0    0  1  0    0
  text3 1  0  0  0    0    0    0  4  0    0
  text4 1  3  0  0    0    0    0  1  0    0
  text5 1  3  0  0    0    0    0  1  0    0
  text6 0  1  0  0    0    0    0  0  0    0
[ reached max_ndoc ... 4 more documents, reached max_nfeat ... 134 more features ]

使用dfm_tfidf()函数,对词频矩阵计算tfidf值,形成tfidf矩阵。


> ssidf<-dfm_tfidf(ssdfm)
> ssidf
Document-feature matrix of: 10 documents, 144 features (84.51% sparse) and 0 docvars.
       features
docs          r         的 极 客 理想 系列 文章         , 涵 盖了
  text1 0.90309 0.48455007  1  1    1    1    1 0.48455007  1    1
  text2 0.30103 0           0  0    0    0    0 0.09691001  0    0
  text3 0.30103 0           0  0    0    0    0 0.38764005  0    0
  text4 0.30103 0.29073004  0  0    0    0    0 0.09691001  0    0
  text5 0.30103 0.29073004  0  0    0    0    0 0.09691001  0    0
  text6 0       0.09691001  0  0    0    0    0 0           0    0
[ reached max_ndoc ... 4 more documents, reached max_nfeat ... 134 more features ]

用词频矩阵计算2个文档的相似性


> ssdfm %>%
+   textstat_simil(method = "cosine") %>%
+   as.matrix()
           text1      text2      text3     text4     text5      text6      text7      text8      text9     text10
text1  1.0000000 0.21426863 0.44799204 0.4716523 0.5079850 0.13525045 0.19705795 0.20087684 0.22392949 0.23823145
text2  0.2142686 1.00000000 0.40996003 0.2702264 0.2425356 0.04950738 0.07868895 0.08823529 0.11803342 0.04756515
text3  0.4479920 0.40996003 1.00000000 0.2197177 0.2366432 0.03450328 0.13710212 0.12298801 0.24678382 0.03314968
text4  0.4716523 0.27022641 0.21971769 1.0000000 0.4828079 0.15161961 0.15061881 0.20266981 0.13555693 0.25492496
text5  0.5079850 0.24253563 0.23664319 0.4828079 1.0000000 0.20412415 0.16222142 0.31529631 0.19466571 0.27456259
text6  0.1352504 0.04950738 0.03450328 0.1516196 0.2041241 1.00000000 0.46358632 0.44556639 0.36424640 0.28022427
text7  0.1970580 0.07868895 0.13710212 0.1506188 0.1622214 0.46358632 1.00000000 0.55082263 0.56578947 0.22269967
text8  0.2008768 0.08823529 0.12298801 0.2026698 0.3152963 0.44556639 0.55082263 1.00000000 0.72787276 0.19026060
text9  0.2239295 0.11803342 0.24678382 0.1355569 0.1946657 0.36424640 0.56578947 0.72787276 1.00000000 0.09544271
text10 0.2382314 0.04756515 0.03314968 0.2549250 0.2745626 0.28022427 0.22269967 0.19026060 0.09544271 1.00000000

用tfidf矩阵计算2个文档的相似性


> ssidf %>%
+   textstat_simil(method = "cosine") %>%
+   as.matrix()
             text1        text2        text3       text4       text5        text6       text7        text8       text9       text10
text1  1.000000000 0.0219479864 0.0513556447 0.024124188 0.028953788 0.0118764224 0.015076777 0.0064026405 0.006519969 0.0136248773
text2  0.021947986 1.0000000000 0.1217612800 0.075175409 0.041645241 0.0001912012 0.001083631 0.0009743142 0.001830469 0.0001785203
text3  0.051355645 0.1217612800 1.0000000000 0.018812586 0.022578816 0.0001504185 0.002943756 0.0023568679 0.005326873 0.0001404424
text4  0.024124188 0.0751754091 0.0188125856 1.000000000 0.026150152 0.0021071189 0.002852493 0.0038248671 0.002336975 0.0037986508
text5  0.028953788 0.0416452412 0.0225788162 0.026150152 1.000000000 0.0157598001 0.003423554 0.0678499967 0.021859872 0.0045591308
text6  0.011876422 0.0001912012 0.0001504185 0.002107119 0.015759800 1.0000000000 0.178921295 0.1558538111 0.128284299 0.1178464227
text7  0.015076777 0.0010836308 0.0029437558 0.002852493 0.003423554 0.1789212950 1.000000000 0.2790485085 0.271452809 0.0899040023
text8  0.006402640 0.0009743142 0.0023568679 0.003824867 0.067849997 0.1558538111 0.279048509 1.0000000000 0.421606371 0.0331304236
text9  0.006519969 0.0018304690 0.0053268734 0.002336975 0.021859872 0.1282842988 0.271452809 0.4216063707 1.000000000 0.0195965092
text10 0.013624877 0.0001785203 0.0001404424 0.003798651 0.004559131 0.1178464227 0.089904002 0.0331304236 0.019596509 1.0000000000

从2个相似性矩阵返回的结果来看,都是第8段和第9段更相似。

本文从TF-IDF的实现的角度,介绍了TF-IDF怎么使用R语言程序进行计算。这些计算就是把文本转成向量化数据的方法。

本文的代码已上传到github:https://github.com/bsspirit/r-string-match/blob/main/tfidf.r

转载请注明出处:
http://blog.fens.me/r-text-tf-idf/