为什么Spark的Word2Vec会返回一个向量?

5
运行Spark的Word2Vec示例,我意识到它接受一个字符串数组并输出一个向量。我的问题是,它不应该返回矩阵而不是向量吗?我期望每个输入单词都有一个向量。但它只返回一个向量!
或者它应该接受字符串而不是字符串数组(一个单词)作为输入。然后,当然,它可以返回一个向量作为输出。但是接受一个字符串数组并返回一个单一的向量对我来说没有意义。
[更新]
根据@Shaido的请求,这里是代码,我进行了微小的更改以打印输出的模式:
public class JavaWord2VecExample {
    public static void main(String[] args) {
        SparkSession spark = SparkSession
                .builder()
                .appName("JavaWord2VecExample")
                .getOrCreate();

        // $example on$
        // Input data: Each row is a bag of words from a sentence or document.
        List<Row> data = Arrays.asList(
                RowFactory.create(Arrays.asList("Hi I heard about Spark".split(" "))),
                RowFactory.create(Arrays.asList("I wish Java could use case classes".split(" "))),
                RowFactory.create(Arrays.asList("Logistic regression models are neat".split(" ")))
        );
        StructType schema = new StructType(new StructField[]{
                new StructField("text", new ArrayType(DataTypes.StringType, true), false, Metadata.empty())
        });
        Dataset<Row> documentDF = spark.createDataFrame(data, schema);

        // Learn a mapping from words to Vectors.
        Word2Vec word2Vec = new Word2Vec()
                .setInputCol("text")
                .setOutputCol("result")
                .setVectorSize(7)
                .setMinCount(0);

        Word2VecModel model = word2Vec.fit(documentDF);
        Dataset<Row> result = model.transform(documentDF);

        for (Row row : result.collectAsList()) {
            List<String> text = row.getList(0);
            System.out.println("Schema: " + row.schema());
            Vector vector = (Vector) row.get(1);
            System.out.println("Text: " + text + " => \nVector: " + vector + "\n");
        }
        // $example off$

        spark.stop();
    }
}

并且它会打印:

Schema: StructType(StructField(text,ArrayType(StringType,true),false), StructField(result,org.apache.spark.ml.linalg.VectorUDT@3bfc3ba7,true))
Text: [Hi, I, heard, about, Spark] => 
Vector: [-0.0033279924420639875,-0.0024428479373455048,0.01406305879354477,0.030621735751628878,0.00792500376701355,0.02839711122214794,-0.02286271695047617]

Schema: StructType(StructField(text,ArrayType(StringType,true),false), StructField(result,org.apache.spark.ml.linalg.VectorUDT@3bfc3ba7,true))
Text: [I, wish, Java, could, use, case, classes] => 
Vector: [-9.96453288410391E-4,-0.013741840076233658,0.013064394239336252,-0.01155538750546319,-0.010510949650779366,0.004538436819400106,-0.0036846946126648356]

Schema: StructType(StructField(text,ArrayType(StringType,true),false), StructField(result,org.apache.spark.ml.linalg.VectorUDT@3bfc3ba7,true))
Text: [Logistic, regression, models, are, neat] => 
Vector: [0.012510885251685977,-0.014472834207117558,0.002779599279165268,0.0022389178164303304,0.012743516173213721,-0.02409198731184006,0.017409833287820222]

如果我说错了,请纠正我,但输入是一个字符串数组,输出是一个单一的向量。而我期望每个单词都被映射成一个向量。

2个回答

8
这是一篇解释Spark合理性的尝试,应该作为对已有编程解释的补充阅读...
首先,如何组合单词嵌入本质上不是Word2Vec模型本身的特征(它关注于个别单词),而是"高阶"模型的问题,例如Sentence2Vec、Paragraph2Vec、Doc2VecWikipedia2Vec等等(我想你还可以列举更多...)。
话虽如此,事实证明,将单词向量组合起来以获得更大文本片段(短语、句子、推文等)的向量表示的第一种方法确实是简单地平均构成单词的向量表示,就像Spark ML所做的那样。
从实践者社区开始,我们有:

如何将单词向量连接成句向量(SO答案):

至少有三种常见的组合嵌入向量的方法; (a)求和,(b)求和和平均或(c)连接。[...]请参阅 gensim.models.doc2vec.Doc2Vec, dm_concatdm_mean - 它允许您使用这三个选项中的任何一个。

Sentence2Vec:流行理论的评估-第一部分(单词向量的简单平均值)(博客文章):

当你有词向量并需要计算句向量时,第一件想到的是什么?只需对它们求平均值吗?是的,这就是我们要在这里做的事情。Sentence2Vec(Github仓库)是Word2Vec的封装器,可以帮助找到具有相似语义含义的其他单词。然而,Word2Vec每次只能获取一个单词,而一个句子由多个单词组成。为了解决这个问题,我编写了Sentence2Vec,它实际上是Word2Vec的包装器。为了获得句子的向量,我只需获得该句子中每个单词的平均向量和。至少对于从业者来说,这种简单地对各个单词向量求平均值的方法似乎并不出人意料。 enter image description here 一个可能的反驳是,博客文章和Stack Overflow答案可以说不是非常可靠的来源;那么研究人员和相关的科学文献呢?事实证明,在这里进行简单的平均值计算也并不罕见:来自句子和文档的分布式表示(Le&Mikolov,Google,ICML 2014)的内容。

enter image description here

来自NILC-USP at SemEval-2017 Task 4: A Multi-view Ensemble for Twitter Sentiment analysis(SemEval 2017,第2.1.2节):

enter image description here


现在应该清楚了,在Spark ML中的特定设计选择远非任意或甚至不寻常;我曾经在博客中写过一些看起来非常荒谬的Spark ML设计选择(请参见Spark 2.0中的分类:“输入验证失败”和其他奇妙故事),但似乎这不是这种情况...

感谢您提供如此周到的答案。这绝对是一个更适合问题的答案。请不要误解,但我仍然相信,选择返回向量而不是矩阵的人犯了一个错误。首先,如果他们返回矩阵,从矩阵到向量的转换可以很容易地在用户代码中完成。我想说的是,他们实现了一个非常实用的算法,但同时通过对结果进行平均值处理完全破坏了它。我认为他们不知道自己做了什么或者有人犯了一个错误。再次感谢。 - Mehran

2

要查看每个单词对应的向量,您可以运行model.getVectors。对于问题中的数据帧(向量大小为3而不是7),这将给出:

+----------+-----------------------------------------------------------------+
|word      |vector                                                           |
+----------+-----------------------------------------------------------------+
|heard     |[0.14950960874557495,-0.11237259954214096,-0.03993036597967148]  |
|are       |[-0.16390761733055115,-0.14509087800979614,0.11349033564329147]  |
|neat      |[0.13949351012706757,0.08127426356077194,0.15970033407211304]    |
|classes   |[0.03703496977686882,0.05841822177171707,-0.02267565205693245]   |
|I         |[-0.018915412947535515,-0.13099457323551178,0.14300788938999176] |
|regression|[0.1529865264892578,0.060659825801849365,0.07735282927751541]    |
|Logistic  |[-0.12702016532421112,0.09839040040969849,-0.10370948910713196]  |
|Spark     |[-0.053579315543174744,0.14673036336898804,-0.002033260650932789]|
|could     |[0.12216471135616302,-0.031169598922133446,-0.1427609771490097]  |
|use       |[0.08246973901987076,0.002503493567928672,-0.0796264186501503]   |
|Hi        |[0.16548289358615875,0.06477408856153488,0.09229831397533417]    |
|models    |[-0.05683165416121483,0.009706663899123669,-0.033789146691560745]|
|case      |[0.11626788973808289,0.10363516956567764,-0.07028932124376297]   |
|about     |[-0.1500445008277893,-0.049380943179130554,0.03307584300637245]  |
|Java      |[-0.04074851796030998,0.02809843420982361,-0.16281810402870178]  |
|wish      |[0.11882393807172775,0.13347993791103363,0.14399205148220062]    |
+----------+-----------------------------------------------------------------+

每个单词的确都有自己的表示方式。然而,当你将一个句子(字符串数组)输入模型时,发生的情况是所有句子中单词的向量被平均在一起。

Github实现中可以看到:

/**
  * Transform a sentence column to a vector column to represent the whole sentence. The transform
  * is performed by averaging all word vectors it contains.
  */
 @Since("2.0.0")
 override def transform(dataset: Dataset[_]): DataFrame = {
 ...

这很容易验证,例如:

Text: [Logistic, regression, models, are, neat] => 
Vector: [-0.011055880039930344,0.020988055132329465,0.042608972638845444]

第一个元素是通过取涉及五个单词的向量的第一个元素的平均值来计算的。
(-0.12702016532421112 + 0.1529865264892578 -0.05683165416121483 -0.16390761733055115 + 0.13949351012706757) / 5

这相当于-0.011055880039930344


我被说服了,但这对我来说没有意义。为什么要对数组取平均值?我的意思是平均值没有价值。每个输入都需要转换为矩阵,而不是向量。在我看来,这种实现是绝对没有用的! - Mehran
1
@Mehran: 不要输入需要转换的句子,而是事先将其拆分为单词,并分别输入单词。然后您将得到一个矩阵。 - Shaido
我认为你的意思是每一行应该只包含一个单词(一个类型为String[]且恰好只有一个元素的列)。虽然我也考虑过这种方法,但我意识到这样做行不通。你看,如果你要将Word2Vec的输出传递给RNN(常见情况),你需要输入句子(而不是单词)。如果你事先将每个句子拆分成单词,那么你就无法回到句子,因为你不知道前一个句子在哪里结束,下一个句子从哪里开始。对我来说,这种实现似乎毫无用处,除非我漏掉了什么。 - Mehran
1
@Mehran:也许你可以加一个id列,标记单词属于哪个句子(但是这样就会失去顺序)。我认为目前在Spark中原生实现这一点并不容易... 当前的实现似乎更侧重于查找单词同义词和文档摘要。 - Shaido
1
@Mehran 我同意(并点赞你的问题),但在我看来,这不是拒绝接受一个好答案的理由(毕竟,最终这个答案回答了问题并指出了这种行为的原因,无论我们是否喜欢Spark人员的理由)- 干杯... - desertnaut
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接