有没有哪两个汉字相似到难以区分?
知乎首答。
本学期的一项实验是聚类分析,正好看到了 @酱紫君 对这个问题的回答
有没有哪两个汉字,相似到难以区分?
深受启发,于是决定以这个题目作为我实验的题目,自己试一试。经过尝试,我发现聚类过程其实不需要十几分钟,可以在十几秒内完成。
结果
(图片由 Word Cloud Generator 生成)
下面是经过筛选的一部分结果,释义来自字海网和看相同部首字哪里,叶典网。
个(U+4E2A) (U+201A4)
* 个:【個】的简体字。
㈠拼音ge4。
①量词:三~月。洗~澡。
②单独的:~人。~性。~位。
③人或物体的大小:高~子。
④加在“昨儿”、“今儿”、“明儿”等后面,与“某日里”相近。
㈡拼音ge3。“自~儿”自己。亦作“自各儿”。
* :同【丁】字。
乹(U+4E79)軋(U+8ECB)
* 乹:同【乾㈠】字。
即乾坤之乾。
* 軋:【轧】的繁体字。
异体:轧
卉(U+5349) (U+20984)
* 卉:拼音hui4
①草的总称:奇花异体~。
②花:奇~怪草。
③姓氏。
* :同【卅】字。三十。
墫(U+58AB)壿(U+58FF)
* 墫:㈠拼音zun1。
古同【樽】,酒杯。
㈡拼音dun1。
古同【蹲】
<注>~从土,【壿】从士。
* 壿:拼音cun1
①舞动的样子。
②喜,乐。
嬎(U+5B0E)嬔(U+5B14)
* 嬎:拼音fan4
①生子多而整齐划一。
②繁殖。
③方言,(禽类)生蛋:鸡~蛋。
* 嬔:拼音fu4
兔崽。
②古同【逊】,谦让恭顺。
晝(U+665D)畫(U+756B)
* 晝:【昼】的繁体字。
* 畫:【画】的繁体字。
晳(U+6673)皙(U+7699)
* 晳:同【晰】字。
* 皙:拼音xi1
①皮肤白:“其民~而瘠。”
②泛指白色:“~帻而衣狸制。”
③一种只开花,不结果的枣树。
由(U+7531)甴(U+7534)
* 由:拼音you2
①原因:原~。事~。理~。~于(介词,表示原因或理由)。
②自,从:~表及里。~衷(出于本心)。
③顺随,听从,归属:~不得。信马~缰。
④经过,经历:必~之路。~来已久。
⑤凭借:~此可知。
⑥古同【犹】,尚且,还。
⑦古同【犹】,犹如,好像。
⑧姓。
* 甴:字海释义「同【由】字。」
补充:百度百科:「曱甴,多用于方言,『曱甴』多指『蟑螂』的意思,也有形容阴险恶毒之人。」
曱(U+66F1)甲(U+7532)
* 曱:拼音yue1
取物。
* 甲:拼音jia3
①天干的第一位,用于作顺序第一的代称:~子。花~(六十岁的人)。
②居于首位的,超过所有其它的:~等。
③古代科举考试成绩名次的分类:一~(名为“进士及第”);二~(名为“进士出身”);三~(名为“同进士出身”)。
④古代军人打仗穿的护身衣服,用皮革或金属叶片制成:盔~。~兵。~士。
⑤现代用金属做成有保护功能的装备:~板。装~车。
⑥某些动物身上有保护功能的硬壳:龟~。
⑦手指或脚趾上的角质硬壳:指~。
⑧旧时户口编制单位:保~。~长。
⑨植物果实的外壳:~坼(外表裂开)。
⑩古同【胛】,肩胛。
朏(U+670F)胐(U+80D0)
* 朏:拼音fei3。
①新月开始生明发光,亦用于农历每月初三日的代称。
②天将明:“渐~微明光八表。
<注>~从月,【胐】从肉。
* 胐:拼音ku1。
①髋,胯骨。
②臀。
朣(U+6723)膧(U+81A7)
* 朣:拼音tong2。
①“~胧”朦胧,不分明,如“月~~以含光兮。”
②“~朦”模糊,不分明,如“吉凶纷错,人用~~。”
<注>~从月,【膧】从肉。
* 膧:拼音chuang2。
“~??”尻骨。
汨(U+6C68)汩(U+6C69)
* 汨:拼音mi4
“~罗江”水名,在中国湖南省。
* 汩:㈠拼音gu3。
①水流的样子:~流(急流)。~~(水流动的声音或样子)。
②治理,疏通:决~九川。
③扰乱:“天公岂物欺,若此~时序”。
④涌出的泉水。
⑤沉没:~没。
㈡拼音yu4。
迅疾的样子:悲风~起。
臼(U+81FC) (U+26951)
* 臼:拼音jiu4
①舂米的器具,用石头或木头制成,中间凹下。
②形状像臼的:~齿。
* :㈠ju2,①叉手。②同【匊】
㈡ju3,同【举】
鶇(U+9D87)鶫(U+9DAB)
* 鶇:【鸫】的繁体字。
* 鶫:拼音dong1
<日本释义>
読音tsugumi。斑鶇。秋天從北方南來的候鳥。
㖈(U+3588)䎛(U+439B)
* 㖈:【䎛】的同形重复字。
地名用字。见**标准“信息交换用汉字编码字符集 第八辅助集”(SJ/T 11239-2001)
<韩国释义>
读音nom。音译字。家伙,小子。
古文書所見奴婢名也。孩童美稱也。
* 䎛:拼音lao3
象声词。与“㖈”似乎完全相同。
曶(U+66F6)㫚(U+3ADA)
* 曶:㈠拼音hu1。
①古通【忽】(a.迅速:“~如神。”b.忽略;忽视:“时人皆~之。”c.极微小的数量单位)。
②古同【旸】。
③古剑名。
㈡拼音hu4。
古通【笏】。
* 㫚:同【昒】字。
拼音hu1,hu4
㈠音呼。①通忽。②同昒。③古剑名。
㈡音户。通笏。
朌(U+670C)肦(U+80A6)
* 朌:拼音ban1。
①颁赐;赋与。
②发布。
<注>~从月,【肦】从肉。
* 肦:拼音fen2。
①头大的样子。
②众多。
朓(U+6713)脁(U+8101)
* 朓:拼音tiao3
①农历月底月亮出现在西方:“朒~警阙,朏魄示冲。”
②盈余:“盈者谓之~,不足者谓之朒。”
<注>~从月,【脁】从肉。
* 脁:拼音tiao4。祭祀用的肉。
朘(U+6718)脧(U+8127)
* 朘:拼音juan1。
①缩;减少:“民日削月~,寖以大穷。”
②剥削:~民脂膏。
<注>~从月,【脧】从肉。
* 脧:㈠拼音juan1。汁少的肉羹。
㈡拼音zui1。男孩的shengzhiqi:“未知牝牡之合而~作。”
芉(U+8289)芊(U+828A)
* 芉:拼音gan3
①薏苡子。
②古同【稈】。
* 芊:拼音qian1
①“~~”草木茂盛。
②“~绵”草木茂盛。亦作“芊眠”。
蕌(U+854C)藠(U+85E0)
* 蕌:同【藟】字。
拼音lei3
①藤:“南有樛木,葛~萦之。”
②缠绕:“网罟相萦~。”
③古通【蕾】,花苞:“梅~粉融连夜开。”
* 藠:拼音jiao4
薤的别称:~子。~头。
过程
采用 DBSCAN 算法。算法的流程如下:
(图片来源:cnblogs.com/aijianiula/p/4339960.html)
其实也没有那么复杂,所以自己用 C++ 实现:
>
template >
<>
class >
DataType>
, >
class >
DistanceType>
>
>
static >
std>
::>
pair >
<>
std>
::>
list >
<>
std>
::>
list >
<>
DataType>
>>>
, >
std>
::>
list >
<>
DataType>
>> >
dbscan
>
(
>
std>
::>
list >
<>
DataType>
> >
dataSet>
,
>
std>
::>
function >
<>
DistanceType>
(>
DataType>
, >
DataType>
)>
> >
dist>
,
>
DistanceType >
maxDist>
,
>
size_t >
minPts
>
)
>
{
>
std>
::>
list >
<>
std>
::>
list >
<>
DataType>
>> >
clusters>
;
>
std>
::>
list >
<>
DataType>
> >
isolatedPoints>
;
>
while>
(>
!>
dataSet>
.>
empty>
())
>
{
>
std>
::>
list >
<>
DataType>
> >
thisCluster>
;
>
thisCluster>
.>
push_back>
(>
dataSet>
.>
front>
());
>
dataSet>
.>
pop_front>
();
>
for
>
(
>
typename >
std>
::>
list >
<>
DataType>
>::>
iterator >
thisClusterIter >
= >
thisCluster>
.>
begin>
();
>
thisClusterIter >
!= >
thisCluster>
.>
end>
();
>
++>
thisClusterIter
>
)
>
{
>
for
>
(
>
typename >
std>
::>
list >
<>
DataType>
>::>
iterator >
dataSetIter >
= >
dataSet>
.>
begin>
();
>
dataSetIter >
!= >
dataSet>
.>
end>
();
>
/* do not need increment here */
>
)
>
{
>
if>
(>
dist>
(>
*>
thisClusterIter>
, >
*>
dataSetIter>
) >
<= >
maxDist>
)
>
{
>
thisCluster>
.>
push_back>
(>
*>
dataSetIter>
);
>
dataSetIter >
= >
dataSet>
.>
erase>
(>
dataSetIter>
);
>
}
>
else
>
{
>
++>
dataSetIter>
;
>
}
>
}
>
}
>
if>
(>
thisCluster>
.>
size>
() >
>= >
minPts>
)
>
clusters>
.>
push_back>
(>
thisCluster>
);
>
else
>
isolatedPoints>
.>
splice>
(>
isolatedPoints>
.>
cend>
(), >
thisCluster>
);
>
}
>
return >
std>
::>
make_pair>
(>
clusters>
, >
isolatedPoints>
);
>
}
接下来需要数据集,首先找了两个字体:
然后用 FreeType 2 把字形转换成点阵。
在将字体中的字形转换为点阵的过程中,会指定一个字体大小(如 21),但是转换得到的点阵不一定是 21*21 的,例如「一」这个字的高度只有 1。上述的 21 是 EM Size,而实际大小可能会更大或更小,最大大小是转换前无法得知的,即转换得到的点阵大小是不定的。
但是,如果每个字的点阵的大小都是固定的,会使后续处理更加方便。所以,需要设置两个变量 maxWidth 和 maxHeight,分别记录最大宽度和最大高度。在后续处理时利用这两个变量,把所有点阵都扩大为大小一样的点阵。
扩大有两种方法,一种是居中,即原来的小点阵位于扩大了的点阵的正中间;一种是放在左上角。最初我使用居中的做法,后来觉得放在左上角也没有什么不好,而且使用居中的方法可能出现这样的问题:如果有两个字本来很相似,但一个偏左一些,一个偏右一些,就会出现错位的情况,而放在左上角则可以避免。
考虑了这么多,功能已经有点复杂了,需要自己用 C 语言实现:
#define chkerr(errcode, errstr)
do { if(errcode) { fputs(errstr, stderr); exit(0);} } while (0)
static inline void fPrintPixel(FILE *fp, unsigned char ch, unsigned int count)
{
chkerr(count > 0x8, "Error: (In function printPixel) count should not greater than 0x8.n");
unsigned char b = 0x80;
while(count--)
{
if(ch & b) fputc('#', fp);
else fputc('-', fp);
b >>= 1;
}
}
static void fPrintGlyph(FILE *fp, FT_Face face, FT_ULong encoding, unsigned int *maxWidth, unsigned int *maxHeight)
{
FT_UInt char_index = FT_Get_Char_Index(face, encoding);
if(char_index)
{
fprintf(fp, "%lxn", encoding);
chkerr(FT_Load_Glyph(face, char_index, FT_LOAD_DEFAULT), "Error when loading glyph!n");
chkerr(FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO), "Error when rendering glyph!n");
unsigned int rows = face->glyph->bitmap.rows;
unsigned int width = face->glyph->bitmap.width;
unsigned int widthMain = width/0x8;
unsigned int widthTail = width%0x8;
int pitch = face->glyph->bitmap.pitch;
unsigned char (*buf)[pitch] = (unsigned char (*)[pitch])face->glyph->bitmap.buffer;
unsigned int i, j;
for(i = 0; i < rows; i++)
{
for(j = 0; j < widthMain; j++)
{
fPrintPixel(fp, buf[i][j], 0x8);
}
fPrintPixel(fp, buf[i][j], widthTail);
fputc('n', fp);
}
if(width > *maxWidth) *maxWidth = width;
if(rows > *maxHeight) *maxHeight = rows;
}
}
接下来指定聚类算法的距离函数,这个就简单了。因为是点阵,可以直接利用汉明距离,也就是比较两个点阵有多少个点不同。
>
struct >
glyph
>
{
>
unsigned >
int >
encoding>
;
>
std>
::>
bitset >
<>
GLYPH_WIDTH>
*>
GLYPH_HEIGHT>
> >
data>
;
>
};
>
static >
unsigned >
int >
hammingDistance>
(>
glyph >
a>
, >
glyph >
b>
)
>
{
>
return >
static_cast >
<>
unsigned >
int>
> >
((>
a>
.>
data >
^ >
b>
.>
data>
).>
count>
());
>
}
然后需要不断调整字的大小和距离阈值两个参数。在某些情况下,一些只有偏旁不同的口字旁、日字旁、目字旁的字会被聚成一类,这不是我们希望的结果。
另外,多尝试几种字体,可以得到更丰富的结果。
关于「凉(U+51C9)」和「凉(U+F979)」
最初选择尝试字形聚类,就是因为看到 @酱紫君 的答案有「凉(U+51C9)」和「凉(U+F979)」,感觉十分有趣。
但是,在我最后的结果中,是没有这一组结果的。其原因有二:
原因之一是,「凉(U+F979)」在 CJK 兼容区(U+F900 - U+FAFF),我把这个区注释掉了:
for(FT_UInt i = 0x4e00; i <= 0x9fea; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs
for(FT_UInt i = 0x3400; i <= 0x4db5; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension A
for(FT_UInt i = 0x20000; i <= 0x2a6d6; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension B
for(FT_UInt i = 0x2a700; i <= 0x2b734; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension C
for(FT_UInt i = 0x2b740; i <= 0x2b81d; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension D
for(FT_UInt i = 0x2b820; i <= 0x2cea1; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension E
for(FT_UInt i = 0x2ceb0; i <= 0x2ebe0; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Unified Ideographs Extension F
/* for(FT_UInt i = 0xf900; i <= 0xfaff; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Compatibility Ideographs
for(FT_UInt i = 0x2f800; i <= 0x2fa1f; i++)
fPrintGlyph(fout, face, i, &maxWidth, &maxHeight); // CJK Compatibility Ideographs Supplement */
这是因为,CJK 兼容区里基本上都是基本区已经存在的汉字,参见
为什么 Unicode 中会存在「凉」和「凉」这样两个极其相像的字符?
所以,如果将这个区进行聚类,基本上整个区都可以与基本区的对应汉字聚为一类,这样的结果就没有意义了。
比如,用 FontForge 打开等距更纱黑体 SC,找到 CJK 兼容区,可以看到这样的情况:
基本上都是基本区已有的字。
不过,在 @酱紫君 的回答中,却只有少数几组这样的字和看相同部首字哪里,没有出现预期的异常现象,由于在该回答中使用的字体是微软雅黑,我们用 FontForge 打开微软雅黑,找到 CJK 兼容区:
可以看到微软雅黑在 CJK 兼容区只有少数几个字的字形,其他字是不存在的,自然也就无法被聚类了。
但是,即使不将这个区注释掉,也是得不到「凉(U+51C9)」和「凉(U+F979)」这一组结果的。这是原因之二:「凉(U+51C9)」和「凉(U+F979)」这两个字符的字形,在我所使用的字体中本来就不相似。
查看将等距更纱黑体 SC 的字形转换为点阵的结果:
51c9
---------#------
##-------##-----
-#--############
-##-------------
--##-#########--
-----##------#--
-----##------#--
-----##------#--
-----#########--
--##-----##-----
--#---#--##-##--
-##--##--##--#--
-#---#---##--##-
##--##---##---##
#---#----##---#-
-------###------
f979
---------##-----
#--------##-----
-##-############
--##------------
--#-------------
------#########-
------#------##-
------#------##-
---#--#------##-
--##--#########-
-##------##-----
-#----##-##-##--
##---##--##--##-
#---##---##---##
---##----##----#
-------####-----
在转换后自动生成的 attributes.h 中,查看到参数 GLYPH_WIDTH = 18,GLYPH_HEIGHT = 17。
计算这两个字的距离时,会将这两个字转换为 18*17 的点阵,经过转换后的结果如下:
51c9: 000000000100000000110000000110000000010011111111111100011000000000000000001101111111110000000001100000010000000001100000010000000001100000010000000001111111110000001100000110000000001000100110110000011001100110010000010001000110011000110011000110001100100010000110001000000000011100000000000000000000000000
f979:
000000000110000000100000000110000000011011111111111100001100000000000000001000000000000000000000111111111000000000100000011000000000100000011000000100100000011000001100111111111000011000000110000000010000110110110000110001100110011000100011000110001100000110000110000100000000011110000000000000000000000000
经过比较可知,这两个字形的距离达到了 59,大大超过了设定的距离阈值 8,所以不会被聚为一类。
- 上一篇:面相上看黎明鼻强耳弱,但是为什么他不渣?
- 下一篇:静海看相算话 第1372章 汤家