Search for:

在 VMWare ESXi 或者类似虚拟机扩容 Ubuntu 根目录

最近下载的大模型越来越多,硬盘已经扛不住了,直接在 VMWare 中修改硬盘配置居然没有生效。查阅了一下资料原来需要修改配置后需要在 Ubuntu 中扩容下 ubuntu-vg 。

以下是具体的操作步骤。

1. VMWare 的操作就不累述了,大家都会。

2. SSH 登录服务器,使用 sudo -s 提权。如果你有直接使用 root 用户的“不良习惯”的话,不需要提权了(废话。

3. 执行 fdisk -l,你会发现有行红字。如果没有的话,请检查下你的虚拟机扩容操作。

4. 执行 fdisk /dev/sda,依次输入 n -> 默认回车 -> 默认回车 -> 默认回车 -> w

$ fdisk /dev/sda

Welcome to fdisk (util-linux 2.34).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

GPT PMBR size mismatch (2147483647 != 4294967295) will be corrected by write.

Command (m for help): n
Partition number (6-128, default 6):
First sector (2147483615-4294967262, default 2147483648):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2147483648-4294967262, default 4294967262):

Created a new partition 6 of type 'Linux filesystem' and of size 1024 GiB.

Command (m for help): w
The partition table has been altered.
Syncing disks.

5. 接着执行下 fdisk -l,你会看到你新扩容的 Disk,我这里是 /dev/sda6

6. 接下来,要使用 /dev/sda6 扩容 ubuntu-vg 分区。

执行:vgextend ubuntu-vg /dev/sda6

# 这里提示,输入y
WARNING: ext4 signature detected on /dev/sda6 at offset 1080. Wipe it? [y/n]: y
  Wiping ext4 signature on /dev/sda6.
  Physical volume "/dev/sda6" successfully created.
  Volume group "ubuntu-vg" successfully extended

执行 vgdisplay,检查一下扩容是否完成。

  --- Volume group ---
  VG Name               ubuntu-vg
  System ID
  Format                lvm2
  Metadata Areas        3
  Metadata Sequence No  6
  VG Access             read/write
  VG Status             resizable
  MAX LV                0
  Cur LV                1
  Open LV               1
  Max PV                0
  Cur PV                3
  Act PV                3
  VG Size               <2.00 TiB
  PE Size               4.00 MiB
  Total PE              523901
  Alloc PE / Size       261758 / 1022.49 GiB
  Free  PE / Size       262143 / <1024.00 GiB
# 主要看上方的 Free PE / Size 是否大于0,尺寸应该就是你需要扩容的容量。

7. 使用 df -TH 获取你系统中 Ubuntu 主分区的名字,我这里是 /dev/mapper/ubuntu--vg-ubuntu--lv

$ df -TH
Filesystem                        Type      Size  Used Avail Use% Mounted on
udev                              devtmpfs   34G     0   34G   0% /dev
tmpfs                             tmpfs     6.8G  1.7M  6.8G   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv ext4      1.1T  776G  260G  75% /
...

执行以下2个命令完成扩容。

$ lvextend /dev/mapper/ubuntu--vg-ubuntu--lv /dev/sda6
$ resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv

8. 最后使用 df -h 检查主分区容量是否已经成功扩容。

$ df -h
Filesystem                         Size  Used Avail Use% Mounted on
udev                                32G     0   32G   0% /dev
tmpfs                              6.3G  1.6M  6.3G   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv  2.0T  723G  1.2T  38% /

大功告成~

用 SwiftUI 的方式进行布局 | 肘子的 Swift 记事本

最近时常有朋友反映,尽管 SwiftUI 的布局系统学习门槛很低,但当真正面对要求较高的设计需求时,好像又无从下手。SwiftUI 真的具备创建复杂用户界面的能力吗?本文将通过用多种手段完成同一需求的方式,展示 SwiftUI 布局系统的强大与灵活,并通过这些示例让开发者对 SwiftUI 的布局逻辑有更多的认识和理解。
— 在以下网站上阅读: fatbobman.com/zh/posts/layout-in-swiftui-way/

WebGL 2 Constant and global variable mappings

https://blackgoku36.github.io/Rice2D-API/js/html/webgl/WebGL2RenderingContext.html

ACTIVE_ATTRIBUTES:Int = 35721
ACTIVE_TEXTURE:Int = 34016
ACTIVE_UNIFORMS:Int = 35718
ACTIVE_UNIFORM_BLOCKS:Int = 35382
ALIASED_LINE_WIDTH_RANGE:Int = 33902
ALIASED_POINT_SIZE_RANGE:Int = 33901
ALPHA:Int = 6406
ALPHA_BITS:Int = 3413
ALREADY_SIGNALED:Int = 37146
ALWAYS:Int = 519
ANY_SAMPLES_PASSED:Int = 35887
ANY_SAMPLES_PASSED_CONSERVATIVE:Int = 36202
ARRAY_BUFFER:Int = 34962
ARRAY_BUFFER_BINDING:Int = 34964
ATTACHED_SHADERS:Int = 35717
BACK:Int = 1029
BLEND:Int = 3042
BLEND_COLOR:Int = 32773
BLEND_DST_ALPHA:Int = 32970
BLEND_DST_RGB:Int = 32968
BLEND_EQUATION:Int = 32777
BLEND_EQUATION_ALPHA:Int = 34877
BLEND_EQUATION_RGB:Int = 32777
BLEND_SRC_ALPHA:Int = 32971
BLEND_SRC_RGB:Int = 32969
BLUE_BITS:Int = 3412
BOOL:Int = 35670
BOOL_VEC2:Int = 35671
BOOL_VEC3:Int = 35672
BOOL_VEC4:Int = 35673
BROWSER_DEFAULT_WEBGL:Int = 37444
BUFFER_SIZE:Int = 34660
BUFFER_USAGE:Int = 34661
BYTE:Int = 5120
CCW:Int = 2305
CLAMP_TO_EDGE:Int = 33071
COLOR:Int = 6144
COLOR_ATTACHMENT0:Int = 36064
COLOR_ATTACHMENT1:Int = 36065
COLOR_ATTACHMENT10:Int = 36074
COLOR_ATTACHMENT11:Int = 36075
COLOR_ATTACHMENT12:Int = 36076
COLOR_ATTACHMENT13:Int = 36077
COLOR_ATTACHMENT14:Int = 36078
COLOR_ATTACHMENT15:Int = 36079
COLOR_ATTACHMENT2:Int = 36066
COLOR_ATTACHMENT3:Int = 36067
COLOR_ATTACHMENT4:Int = 36068
COLOR_ATTACHMENT5:Int = 36069
COLOR_ATTACHMENT6:Int = 36070
COLOR_ATTACHMENT7:Int = 36071
COLOR_ATTACHMENT8:Int = 36072
COLOR_ATTACHMENT9:Int = 36073
COLOR_BUFFER_BIT:Int = 16384
COLOR_CLEAR_VALUE:Int = 3106
COLOR_WRITEMASK:Int = 3107
COMPARE_REF_TO_TEXTURE:Int = 34894
COMPILE_STATUS:Int = 35713
COMPRESSED_TEXTURE_FORMATS:Int = 34467
CONDITION_SATISFIED:Int = 37148
CONSTANT_ALPHA:Int = 32771
CONSTANT_COLOR:Int = 32769
CONTEXT_LOST_WEBGL:Int = 37442
COPY_READ_BUFFER:Int = 36662
COPY_READ_BUFFER_BINDING:Int = 36662
COPY_WRITE_BUFFER:Int = 36663
COPY_WRITE_BUFFER_BINDING:Int = 36663
CULL_FACE:Int = 2884
CULL_FACE_MODE:Int = 2885
CURRENT_PROGRAM:Int = 35725
CURRENT_QUERY:Int = 34917
CURRENT_VERTEX_ATTRIB:Int = 34342
CW:Int = 2304
DECR:Int = 7683
DECR_WRAP:Int = 34056
DELETE_STATUS:Int = 35712
DEPTH:Int = 6145
DEPTH24_STENCIL8:Int = 35056
DEPTH32F_STENCIL8:Int = 36013
DEPTH_ATTACHMENT:Int = 36096
DEPTH_BITS:Int = 3414
DEPTH_BUFFER_BIT:Int = 256
DEPTH_CLEAR_VALUE:Int = 2931
DEPTH_COMPONENT:Int = 6402
DEPTH_COMPONENT16:Int = 33189
DEPTH_COMPONENT24:Int = 33190
DEPTH_COMPONENT32F:Int = 36012
DEPTH_FUNC:Int = 2932
DEPTH_RANGE:Int = 2928
DEPTH_STENCIL:Int = 34041
DEPTH_STENCIL_ATTACHMENT:Int = 33306
DEPTH_TEST:Int = 2929
DEPTH_WRITEMASK:Int = 2930
DITHER:Int = 3024
DONT_CARE:Int = 4352
DRAW_BUFFER0:Int = 34853
DRAW_BUFFER1:Int = 34854
DRAW_BUFFER10:Int = 34863
DRAW_BUFFER11:Int = 34864
DRAW_BUFFER12:Int = 34865
DRAW_BUFFER13:Int = 34866
DRAW_BUFFER14:Int = 34867
DRAW_BUFFER15:Int = 34868
DRAW_BUFFER2:Int = 34855
DRAW_BUFFER3:Int = 34856
DRAW_BUFFER4:Int = 34857
DRAW_BUFFER5:Int = 34858
DRAW_BUFFER6:Int = 34859
DRAW_BUFFER7:Int = 34860
DRAW_BUFFER8:Int = 34861
DRAW_BUFFER9:Int = 34862
DRAW_FRAMEBUFFER:Int = 36009
DRAW_FRAMEBUFFER_BINDING:Int = 36006
DST_ALPHA:Int = 772
DST_COLOR:Int = 774
DYNAMIC_COPY:Int = 35050
DYNAMIC_DRAW:Int = 35048
DYNAMIC_READ:Int = 35049
ELEMENT_ARRAY_BUFFER:Int = 34963
ELEMENT_ARRAY_BUFFER_BINDING:Int = 34965
EQUAL:Int = 514
FASTEST:Int = 4353
FLOAT:Int = 5126
FLOAT_32_UNSIGNED_INT_24_8_REV:Int = 36269
FLOAT_MAT2:Int = 35674
FLOAT_MAT2x3:Int = 35685
FLOAT_MAT2x4:Int = 35686
FLOAT_MAT3:Int = 35675
FLOAT_MAT3x2:Int = 35687
FLOAT_MAT3x4:Int = 35688
FLOAT_MAT4:Int = 35676
FLOAT_MAT4x2:Int = 35689
FLOAT_MAT4x3:Int = 35690
FLOAT_VEC2:Int = 35664
FLOAT_VEC3:Int = 35665
FLOAT_VEC4:Int = 35666
FRAGMENT_SHADER:Int = 35632
FRAGMENT_SHADER_DERIVATIVE_HINT:Int = 35723
FRAMEBUFFER:Int = 36160
FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:Int = 33301
FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:Int = 33300
FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:Int = 33296
FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:Int = 33297
FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:Int = 33302
FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:Int = 33299
FRAMEBUFFER_ATTACHMENT_OBJECT_NAME:Int = 36049
FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:Int = 36048
FRAMEBUFFER_ATTACHMENT_RED_SIZE:Int = 33298
FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:Int = 33303
FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE:Int = 36051
FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER:Int = 36052
FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL:Int = 36050
FRAMEBUFFER_BINDING:Int = 36006
FRAMEBUFFER_COMPLETE:Int = 36053
FRAMEBUFFER_DEFAULT:Int = 33304
FRAMEBUFFER_INCOMPLETE_ATTACHMENT:Int = 36054
FRAMEBUFFER_INCOMPLETE_DIMENSIONS:Int = 36057
FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:Int = 36055
FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:Int = 36182
FRAMEBUFFER_UNSUPPORTED:Int = 36061
FRONT:Int = 1028
FRONT_AND_BACK:Int = 1032
FRONT_FACE:Int = 2886
FUNC_ADD:Int = 32774
FUNC_REVERSE_SUBTRACT:Int = 32779
FUNC_SUBTRACT:Int = 32778
GENERATE_MIPMAP_HINT:Int = 33170
GEQUAL:Int = 518
GREATER:Int = 516
GREEN_BITS:Int = 3411
HALF_FLOAT:Int = 5131
HIGH_FLOAT:Int = 36338
HIGH_INT:Int = 36341
IMPLEMENTATION_COLOR_READ_FORMAT:Int = 35739
IMPLEMENTATION_COLOR_READ_TYPE:Int = 35738
INCR:Int = 7682
INCR_WRAP:Int = 34055
INT:Int = 5124
INTERLEAVED_ATTRIBS:Int = 35980
INT_2_10_10_10_REV:Int = 36255
INT_SAMPLER_2D:Int = 36298
INT_SAMPLER_2D_ARRAY:Int = 36303
INT_SAMPLER_3D:Int = 36299
INT_SAMPLER_CUBE:Int = 36300
INT_VEC2:Int = 35667
INT_VEC3:Int = 35668
INT_VEC4:Int = 35669
INVALID_ENUM:Int = 1280
INVALID_FRAMEBUFFER_OPERATION:Int = 1286
INVALID_INDEX:Int = cast 4294967295
INVALID_OPERATION:Int = 1282
INVALID_VALUE:Int = 1281
INVERT:Int = 5386
KEEP:Int = 7680
LEQUAL:Int = 515
LESS:Int = 513
LINEAR:Int = 9729
LINEAR_MIPMAP_LINEAR:Int = 9987
LINEAR_MIPMAP_NEAREST:Int = 9985
LINES:Int = 1
LINE_LOOP:Int = 2
LINE_STRIP:Int = 3
LINE_WIDTH:Int = 2849
LINK_STATUS:Int = 35714
LOW_FLOAT:Int = 36336
LOW_INT:Int = 36339
LUMINANCE:Int = 6409
LUMINANCE_ALPHA:Int = 6410
MAX:Int = 32776
MAX_3D_TEXTURE_SIZE:Int = 32883
MAX_ARRAY_TEXTURE_LAYERS:Int = 35071
MAX_CLIENT_WAIT_TIMEOUT_WEBGL:Int = 37447
MAX_COLOR_ATTACHMENTS:Int = 36063
MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:Int = 35379
MAX_COMBINED_TEXTURE_IMAGE_UNITS:Int = 35661
MAX_COMBINED_UNIFORM_BLOCKS:Int = 35374
MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:Int = 35377
MAX_CUBE_MAP_TEXTURE_SIZE:Int = 34076
MAX_DRAW_BUFFERS:Int = 34852
MAX_ELEMENTS_INDICES:Int = 33001
MAX_ELEMENTS_VERTICES:Int = 33000
MAX_ELEMENT_INDEX:Int = 36203
MAX_FRAGMENT_INPUT_COMPONENTS:Int = 37157
MAX_FRAGMENT_UNIFORM_BLOCKS:Int = 35373
MAX_FRAGMENT_UNIFORM_COMPONENTS:Int = 35657
MAX_FRAGMENT_UNIFORM_VECTORS:Int = 36349
MAX_PROGRAM_TEXEL_OFFSET:Int = 35077
MAX_RENDERBUFFER_SIZE:Int = 34024
MAX_SAMPLES:Int = 36183
MAX_SERVER_WAIT_TIMEOUT:Int = 37137
MAX_TEXTURE_IMAGE_UNITS:Int = 34930
MAX_TEXTURE_LOD_BIAS:Int = 34045
MAX_TEXTURE_SIZE:Int = 3379
MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS:Int = 35978
MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS:Int = 35979
MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:Int = 35968
MAX_UNIFORM_BLOCK_SIZE:Int = 35376
MAX_UNIFORM_BUFFER_BINDINGS:Int = 35375
MAX_VARYING_COMPONENTS:Int = 35659
MAX_VARYING_VECTORS:Int = 36348
MAX_VERTEX_ATTRIBS:Int = 34921
MAX_VERTEX_OUTPUT_COMPONENTS:Int = 37154
MAX_VERTEX_TEXTURE_IMAGE_UNITS:Int = 35660
MAX_VERTEX_UNIFORM_BLOCKS:Int = 35371
MAX_VERTEX_UNIFORM_COMPONENTS:Int = 35658
MAX_VERTEX_UNIFORM_VECTORS:Int = 36347
MAX_VIEWPORT_DIMS:Int = 3386
MEDIUM_FLOAT:Int = 36337
MEDIUM_INT:Int = 36340
MIN:Int = 32775
MIN_PROGRAM_TEXEL_OFFSET:Int = 35076
MIRRORED_REPEAT:Int = 33648
NEAREST:Int = 9728
NEAREST_MIPMAP_LINEAR:Int = 9986
NEAREST_MIPMAP_NEAREST:Int = 9984
NEVER:Int = 512
NICEST:Int = 4354
NONE:Int = 0
NOTEQUAL:Int = 517
NO_ERROR:Int = 0
OBJECT_TYPE:Int = 37138
ONE:Int = 1
ONE_MINUS_CONSTANT_ALPHA:Int = 32772
ONE_MINUS_CONSTANT_COLOR:Int = 32770
ONE_MINUS_DST_ALPHA:Int = 773
ONE_MINUS_DST_COLOR:Int = 775
ONE_MINUS_SRC_ALPHA:Int = 771
ONE_MINUS_SRC_COLOR:Int = 769
OUT_OF_MEMORY:Int = 1285
PACK_ALIGNMENT:Int = 3333
PACK_ROW_LENGTH:Int = 3330
PACK_SKIP_PIXELS:Int = 3332
PACK_SKIP_ROWS:Int = 3331
PIXEL_PACK_BUFFER:Int = 35051
PIXEL_PACK_BUFFER_BINDING:Int = 35053
PIXEL_UNPACK_BUFFER:Int = 35052
PIXEL_UNPACK_BUFFER_BINDING:Int = 35055
POINTS:Int = 0
POLYGON_OFFSET_FACTOR:Int = 32824
POLYGON_OFFSET_FILL:Int = 32823
POLYGON_OFFSET_UNITS:Int = 10752
QUERY_RESULT:Int = 34918
QUERY_RESULT_AVAILABLE:Int = 34919
R11F_G11F_B10F:Int = 35898
R16F:Int = 33325
R16I:Int = 33331
R16UI:Int = 33332
R32F:Int = 33326
R32I:Int = 33333
R32UI:Int = 33334
R8:Int = 33321
R8I:Int = 33329
R8UI:Int = 33330
R8_SNORM:Int = 36756
RASTERIZER_DISCARD:Int = 35977
READ_BUFFER:Int = 3074
READ_FRAMEBUFFER:Int = 36008
READ_FRAMEBUFFER_BINDING:Int = 36010
RED:Int = 6403
RED_BITS:Int = 3410
RED_INTEGER:Int = 36244
RENDERBUFFER:Int = 36161
RENDERBUFFER_ALPHA_SIZE:Int = 36179
RENDERBUFFER_BINDING:Int = 36007
RENDERBUFFER_BLUE_SIZE:Int = 36178
RENDERBUFFER_DEPTH_SIZE:Int = 36180
RENDERBUFFER_GREEN_SIZE:Int = 36177
RENDERBUFFER_HEIGHT:Int = 36163
RENDERBUFFER_INTERNAL_FORMAT:Int = 36164
RENDERBUFFER_RED_SIZE:Int = 36176
RENDERBUFFER_SAMPLES:Int = 36011
RENDERBUFFER_STENCIL_SIZE:Int = 36181
RENDERBUFFER_WIDTH:Int = 36162
RENDERER:Int = 7937
REPEAT:Int = 10497
REPLACE:Int = 7681
RG:Int = 33319
RG16F:Int = 33327
RG16I:Int = 33337
RG16UI:Int = 33338
RG32F:Int = 33328
RG32I:Int = 33339
RG32UI:Int = 33340
RG8:Int = 33323
RG8I:Int = 33335
RG8UI:Int = 33336
RG8_SNORM:Int = 36757
RGB:Int = 6407
RGB10_A2:Int = 32857
RGB10_A2UI:Int = 36975
RGB16F:Int = 34843
RGB16I:Int = 36233
RGB16UI:Int = 36215
RGB32F:Int = 34837
RGB32I:Int = 36227
RGB32UI:Int = 36209
RGB565:Int = 36194
RGB5_A1:Int = 32855
RGB8:Int = 32849
RGB8I:Int = 36239
RGB8UI:Int = 36221
RGB8_SNORM:Int = 36758
RGB9_E5:Int = 35901
RGBA:Int = 6408
RGBA16F:Int = 34842
RGBA16I:Int = 36232
RGBA16UI:Int = 36214
RGBA32F:Int = 34836
RGBA32I:Int = 36226
RGBA32UI:Int = 36208
RGBA4:Int = 32854
RGBA8:Int = 32856
RGBA8I:Int = 36238
RGBA8UI:Int = 36220
RGBA8_SNORM:Int = 36759
RGBA_INTEGER:Int = 36249
RGB_INTEGER:Int = 36248
RG_INTEGER:Int = 33320
SAMPLER_2D:Int = 35678
SAMPLER_2D_ARRAY:Int = 36289
SAMPLER_2D_ARRAY_SHADOW:Int = 36292
SAMPLER_2D_SHADOW:Int = 35682
SAMPLER_3D:Int = 35679
SAMPLER_BINDING:Int = 35097
SAMPLER_CUBE:Int = 35680
SAMPLER_CUBE_SHADOW:Int = 36293
SAMPLES:Int = 32937
SAMPLE_ALPHA_TO_COVERAGE:Int = 32926
SAMPLE_BUFFERS:Int = 32936
SAMPLE_COVERAGE:Int = 32928
SAMPLE_COVERAGE_INVERT:Int = 32939
SAMPLE_COVERAGE_VALUE:Int = 32938
SCISSOR_BOX:Int = 3088
SCISSOR_TEST:Int = 3089
SEPARATE_ATTRIBS:Int = 35981
SHADER_TYPE:Int = 35663
SHADING_LANGUAGE_VERSION:Int = 35724
SHORT:Int = 5122
SIGNALED:Int = 37145
SIGNED_NORMALIZED:Int = 36764
SRC_ALPHA:Int = 770
SRC_ALPHA_SATURATE:Int = 776
SRC_COLOR:Int = 768
SRGB:Int = 35904
SRGB8:Int = 35905
SRGB8_ALPHA8:Int = 35907
STATIC_COPY:Int = 35046
STATIC_DRAW:Int = 35044
STATIC_READ:Int = 35045
STENCIL:Int = 6146
STENCIL_ATTACHMENT:Int = 36128
STENCIL_BACK_FAIL:Int = 34817
STENCIL_BACK_FUNC:Int = 34816
STENCIL_BACK_PASS_DEPTH_FAIL:Int = 34818
STENCIL_BACK_PASS_DEPTH_PASS:Int = 34819
STENCIL_BACK_REF:Int = 36003
STENCIL_BACK_VALUE_MASK:Int = 36004
STENCIL_BACK_WRITEMASK:Int = 36005
STENCIL_BITS:Int = 3415
STENCIL_BUFFER_BIT:Int = 1024
STENCIL_CLEAR_VALUE:Int = 2961
STENCIL_FAIL:Int = 2964
STENCIL_FUNC:Int = 2962
STENCIL_INDEX8:Int = 36168
STENCIL_PASS_DEPTH_FAIL:Int = 2965
STENCIL_PASS_DEPTH_PASS:Int = 2966
STENCIL_REF:Int = 2967
STENCIL_TEST:Int = 2960
STENCIL_VALUE_MASK:Int = 2963
STENCIL_WRITEMASK:Int = 2968
STREAM_COPY:Int = 35042
STREAM_DRAW:Int = 35040
STREAM_READ:Int = 35041
SUBPIXEL_BITS:Int = 3408
SYNC_CONDITION:Int = 37139
SYNC_FENCE:Int = 37142
SYNC_FLAGS:Int = 37141
SYNC_FLUSH_COMMANDS_BIT:Int = 1
SYNC_GPU_COMMANDS_COMPLETE:Int = 37143
SYNC_STATUS:Int = 37140
TEXTURE:Int = 5890
TEXTURE0:Int = 33984
TEXTURE1:Int = 33985
TEXTURE10:Int = 33994
TEXTURE11:Int = 33995
TEXTURE12:Int = 33996
TEXTURE13:Int = 33997
TEXTURE14:Int = 33998
TEXTURE15:Int = 33999
TEXTURE16:Int = 34000
TEXTURE17:Int = 34001
TEXTURE18:Int = 34002
TEXTURE19:Int = 34003
TEXTURE2:Int = 33986
TEXTURE20:Int = 34004
TEXTURE21:Int = 34005
TEXTURE22:Int = 34006
TEXTURE23:Int = 34007
TEXTURE24:Int = 34008
TEXTURE25:Int = 34009
TEXTURE26:Int = 34010
TEXTURE27:Int = 34011
TEXTURE28:Int = 34012
TEXTURE29:Int = 34013
TEXTURE3:Int = 33987
TEXTURE30:Int = 34014
TEXTURE31:Int = 34015
TEXTURE4:Int = 33988
TEXTURE5:Int = 33989
TEXTURE6:Int = 33990
TEXTURE7:Int = 33991
TEXTURE8:Int = 33992
TEXTURE9:Int = 33993
TEXTURE_2D:Int = 3553
TEXTURE_2D_ARRAY:Int = 35866
TEXTURE_3D:Int = 32879
TEXTURE_BASE_LEVEL:Int = 33084
TEXTURE_BINDING_2D:Int = 32873
TEXTURE_BINDING_2D_ARRAY:Int = 35869
TEXTURE_BINDING_3D:Int = 32874
TEXTURE_BINDING_CUBE_MAP:Int = 34068
TEXTURE_COMPARE_FUNC:Int = 34893
TEXTURE_COMPARE_MODE:Int = 34892
TEXTURE_CUBE_MAP:Int = 34067
TEXTURE_CUBE_MAP_NEGATIVE_X:Int = 34070
TEXTURE_CUBE_MAP_NEGATIVE_Y:Int = 34072
TEXTURE_CUBE_MAP_NEGATIVE_Z:Int = 34074
TEXTURE_CUBE_MAP_POSITIVE_X:Int = 34069
TEXTURE_CUBE_MAP_POSITIVE_Y:Int = 34071
TEXTURE_CUBE_MAP_POSITIVE_Z:Int = 34073
TEXTURE_IMMUTABLE_FORMAT:Int = 37167
TEXTURE_IMMUTABLE_LEVELS:Int = 33503
TEXTURE_MAG_FILTER:Int = 10240
TEXTURE_MAX_LEVEL:Int = 33085
TEXTURE_MAX_LOD:Int = 33083
TEXTURE_MIN_FILTER:Int = 10241
TEXTURE_MIN_LOD:Int = 33082
TEXTURE_WRAP_R:Int = 32882
TEXTURE_WRAP_S:Int = 10242
TEXTURE_WRAP_T:Int = 10243
TIMEOUT_EXPIRED:Int = 37147
TIMEOUT_IGNORED:Int = -1
TRANSFORM_FEEDBACK:Int = 36386
TRANSFORM_FEEDBACK_ACTIVE:Int = 36388
TRANSFORM_FEEDBACK_BINDING:Int = 36389
TRANSFORM_FEEDBACK_BUFFER:Int = 35982
TRANSFORM_FEEDBACK_BUFFER_BINDING:Int = 35983
TRANSFORM_FEEDBACK_BUFFER_MODE:Int = 35967
TRANSFORM_FEEDBACK_BUFFER_SIZE:Int = 35973
TRANSFORM_FEEDBACK_BUFFER_START:Int = 35972
TRANSFORM_FEEDBACK_PAUSED:Int = 36387
TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:Int = 35976
TRANSFORM_FEEDBACK_VARYINGS:Int = 35971
TRIANGLES:Int = 4
TRIANGLE_FAN:Int = 6
TRIANGLE_STRIP:Int = 5
UNIFORM_ARRAY_STRIDE:Int = 35388
UNIFORM_BLOCK_ACTIVE_UNIFORMS:Int = 35394
UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES:Int = 35395
UNIFORM_BLOCK_BINDING:Int = 35391
UNIFORM_BLOCK_DATA_SIZE:Int = 35392
UNIFORM_BLOCK_INDEX:Int = 35386
UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:Int = 35398
UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:Int = 35396
UNIFORM_BUFFER:Int = 35345
UNIFORM_BUFFER_BINDING:Int = 35368
UNIFORM_BUFFER_OFFSET_ALIGNMENT:Int = 35380
UNIFORM_BUFFER_SIZE:Int = 35370
UNIFORM_BUFFER_START:Int = 35369
UNIFORM_IS_ROW_MAJOR:Int = 35390
UNIFORM_MATRIX_STRIDE:Int = 35389
UNIFORM_OFFSET:Int = 35387
UNIFORM_SIZE:Int = 35384
UNIFORM_TYPE:Int = 35383
UNPACK_ALIGNMENT:Int = 3317
UNPACK_COLORSPACE_CONVERSION_WEBGL:Int = 37443
UNPACK_FLIP_Y_WEBGL:Int = 37440
UNPACK_IMAGE_HEIGHT:Int = 32878
UNPACK_PREMULTIPLY_ALPHA_WEBGL:Int = 37441
UNPACK_ROW_LENGTH:Int = 3314
UNPACK_SKIP_IMAGES:Int = 32877
UNPACK_SKIP_PIXELS:Int = 3316
UNPACK_SKIP_ROWS:Int = 3315
UNSIGNALED:Int = 37144
UNSIGNED_BYTE:Int = 5121
UNSIGNED_INT:Int = 5125
UNSIGNED_INT_10F_11F_11F_REV:Int = 35899
UNSIGNED_INT_24_8:Int = 34042
UNSIGNED_INT_2_10_10_10_REV:Int = 33640
UNSIGNED_INT_5_9_9_9_REV:Int = 35902
UNSIGNED_INT_SAMPLER_2D:Int = 36306
UNSIGNED_INT_SAMPLER_2D_ARRAY:Int = 36311
UNSIGNED_INT_SAMPLER_3D:Int = 36307
UNSIGNED_INT_SAMPLER_CUBE:Int = 36308
UNSIGNED_INT_VEC2:Int = 36294
UNSIGNED_INT_VEC3:Int = 36295
UNSIGNED_INT_VEC4:Int = 36296
UNSIGNED_NORMALIZED:Int = 35863
UNSIGNED_SHORT:Int = 5123
UNSIGNED_SHORT_4_4_4_4:Int = 32819
UNSIGNED_SHORT_5_5_5_1:Int = 32820
UNSIGNED_SHORT_5_6_5:Int = 33635
VALIDATE_STATUS:Int = 35715
VENDOR:Int = 7936
VERSION:Int = 7938
VERTEX_ARRAY_BINDING:Int = 34229
VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:Int = 34975
VERTEX_ATTRIB_ARRAY_DIVISOR:Int = 35070
VERTEX_ATTRIB_ARRAY_ENABLED:Int = 34338
VERTEX_ATTRIB_ARRAY_INTEGER:Int = 35069
VERTEX_ATTRIB_ARRAY_NORMALIZED:Int = 34922
VERTEX_ATTRIB_ARRAY_POINTER:Int = 34373
VERTEX_ATTRIB_ARRAY_SIZE:Int = 34339
VERTEX_ATTRIB_ARRAY_STRIDE:Int = 34340
VERTEX_ATTRIB_ARRAY_TYPE:Int = 34341
VERTEX_SHADER:Int = 35633
VIEWPORT:Int = 2978
WAIT_FAILED:Int = 37149
ZERO:Int = 0

SDXL LoRA 训练笔记

准备素材

  1. 尺寸:没有要求。
  2. 如果是人物,需要多角度。

前言

如果在大局域网内,可以使用以下镜像下模型、代码。

使用方法见网站上的说明。

  1. https://ghproxy.com/
  2. https://hf-mirror.com/

部署 kohya-ss

地址:https://github.com/bmaltais/kohya_ss

在 ubuntu 下部署

$ conda create --name kohya python=3.10
$ conda activate kohya
# 安装 cudaNN 在 https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64
$ git clone https://github.com/bmaltais/kohya_ss.git
$ cd kohya_ss
$ chmod +x ./setup.sh
$ ./setup.sh

启动命令

$ ./gui.sh --listen 0.0.0.0 --server_port 5000

素材预处理

WebUI 安装插件:https://github.com/picobyte/stable-diffusion-webui-wd14-tagger

  1. 选择 Tagger 标签
  2. 先用一张图选择适合的反推算法
  3. 进行批量操作批量生成打标的图片集

标签批量处理工具

  1. windows
  2. mac

配置

打开 http://xxx:5000

底模配置

  1. 选择 LoRA 标签
  2. 在 Traning – Source model – Model Quick Pick 中选择 custom
  3. Pretrained model name or path 中填入 sd_xl_base_1.0.safetensors 的路径
  4. 点选 SDXL Model

准备训练集

首先将训练的素材上传到服务器目录。

  1. 选择 Tools 标签
  2. 选择 Dataset Preparation 标签
  3. 在 Instance prompt 填写 主要触发词
  4. 在 Class prompt 填写分类触发词
  5. 在 Training images 填写数据集路径
  6. 在 Repeats 填写循环次数,比如 30 次
  7. 在 Destination training directory 填写训练集输出目录
  8. 点击 Prepare training data 开始预训练

网页上不会有提示,看后台日志。这个应该很快。

最后点击 Copy info to Folders Tab 将信息同步,进入下一步。

训练参数设定

这个训练参数适合 24G 以上显存

  1. 点击 Traning – Folders,检查参数是否代入正确

  2. 填写 Model output name 模型输出名字

  3. 点击 Parameters 标签

  4. 修改以下设置:

    1. LoRA Type: LyCORIS/LoHa
    2. Train batch size: 8
    3. 勾选 Cache latents 和 Cache latents to disk
    4. Max resolution: 1024,1024
    5. Epoch:5
    6. Caption Extension:.txt
    7. Optimizer: Prodigy
    8. Learning rate: 1
    9. Text Encoder learning rate: 1
    10. Unet learning rate: 1
    11. LR Scheduler: constant
    12. LR warmup (% of steps): 0
    13. Optimizer extra arguments: –optimizer_args “safeguard_warmup=True” “weight_decay=0.1” “decouple=True” “use_bias_correction=True” “d_coef=0.5” –network_train_unet_only
    14. Network Rank (Dimension): 8
    15. Convolution Rank (Dimension): 4
    16. Network Alpha:1
    17. Convolution Alpha: 1
    18. Seed: 1234
    19. (在 Advanced 标签) Noise offset:0.0357
    20. (在 Advanced 标签) 勾选 Gradient checkpointing

最后,点击开始训练,日志在后台!

Core ML 和 Vision 教程:在 iOS 设备上直接进行模型微调

原文: https://www.raywenderlich.com/7960296-core-ml-and-vision-tutorial-on-device-training-on-ios

本教程向你介绍 Core ML 和 Vision 这两个最新的 iOS 框架,以及如何在设备上“微调”模型。

工程文件下载:https://koenig-media.raywenderlich.com/uploads/2020/05/Vibes-1.zip

译者传了百度网盘分享,如果上方的链接失效了可以点击 这里,提取码是 vec5

原文运行环境:Swift 5, iOS 13, Xcode 11

Update note: Christine Abernathy updated this tutorial for Xcode 11, Swift 5 and iOS 13. Audrey Tam wrote the original. 译者已经将代码提升兼容至 Swift 5.4, iOS 13, Xcode 12.5

苹果在iOS 11中发布了 Core MLVisionCore ML 为开发者提供了将机器学习模型引入App的方法。从而可以实现在iOS设备上直接使用 AI 相关的功能,比如物体检测。

iOS 13 在 Core ML 3 中增加了 on-device training (基于设备的模型训练)功能,为框架增加了基于设备进行模型个性化微调能力。

在本教程中,你将学习如何使用 Core MLVision 框架在设备上“微调”一个模型。为了学习这些内容,你可以从文章开始的链接下载一个示例的工程 Vibes ,这是一个根据所选图像随机生成箴言的应用程序。在完成教程后,这个项目还能在训练模型后使用“快捷方式”添加你指定的表情符号并且放到绘制“快捷方式”的位置。

开端

开始前,从文章开头下载示例的工程。下载文件是一个 zip 压缩包,将这个压缩包解压后,你会看到 starterfinal 这两个文件夹。双击在 starter 中的 Vibes.xcodeproj 打开项目。

编译并且运行(如果你要在真机上运行,需要改BundleID,译者注)项目,你会看到这个界面。

Vibes-Starter-Project

点击左下角的相机按钮,选择一个相册中预置(模拟器会自带一些图片,最好使用模拟器完成这个教程,译者注)的照片,可以看到下发自动生成了箴言。

接下来,点击右下角表情符号的图标,选择一个 emoji 表情贴纸添加到图片上。可以使用手指,将贴纸移动到任何位置。

Vibes starter app flow: Blank screen, waterfall image with quote, selection of emojis, waterfall with quote and emoji

基于这个初始的项目,我们有2点可以改进:

Little bug guy saying Do tell
  1. 箴言似乎是随机生成的,为何不生成和图片最相关的箴言呢?
  2. 表情符号贴纸添加起来太麻烦了,有没有更方便的添加贴纸的方式呢?

你在本教程中的目标是使用机器学习来解决这两个问题。

什么是机器学习

如果你是机器学习的新手,以下是对于这个领域术语的一些解释。

人工智能(AI)定义为:以编程方式添加到机器上以模仿人类的行动和思想的能力。

机器学习(ML)是人工智能的子集,训练机器执行某些特定任务。例如,你可以使用 ML 来训练机器识别图像中的猫,或将文字从一种语言翻译成另一种语言。

深度学习是一种机器训练方法。这种技术模仿人脑,由组织在网络中的神经元组成。深度学习从提供的数据中训练出一个人工神经网络。

Phrases going into a microphone, an arrow pointing to a web of dots and lines

假设你想让机器识别图像中的一只猫。你可以给机器提供大量人工标注为 是猫不是猫 的图像。然后你可以从中建立一个模型,使用这个模型可以对另外的图片做出准确的猜测或预测。

使用模型进行训练

苹果将模型 (Model) 定义为“将机器学习算法应用于一组训练数据的结果”。把模型看作是一个函数,它接受一个输入,对给定的输入进行特定的操作,使其达到最佳效果,比如学习,然后进行预测和分类,并产生合适的输出。

A cat, arrow to web of dots and lines, arrow to the word cat

用标记的数据进行训练被称为监督学习(supervised learning)。你需要大量的优质数据来建立一个优质模型。什么是 优质 ?优质数据要尽可能全面,因为最终建立的模型全部依赖于喂给机器的数据。

比如,如果你想让你的模型识别所有的猫,但只给它提供一个特定的品种,它可能会不认识在这些品种之外的猫。用残缺的数据进行训练会导致不想要的结果。

Yoda head, arrow to confused little guy, arrow to cat??

训练过程是计算密集型的,通常在服务器上完成。凭借其并行计算能力,使用 GPU 通常会加快训练的速度。

一旦训练完成,你可以将你的模型部署到生产中,在真实世界的数据上运行预测或推理。

Machine learning workflow: cylinder stack, arrow to web of dots and lines, arrow to rectangle labeled ML

预测推理并不像训练那样需要计算。然而在过去,移动 App 必须远程调用服务器接口才能进行模型推理。现在,移动芯片性能的进步为设备上的推理打开了大门。其好处包括减少延迟,减少对网络的依赖和改善隐私。但是,由于推理运算提高了硬件负载,应用程序大小会增加,推理时电池消耗也会有明显的提升。

本教程展示了如何使用 Core ML 进行基于设备的推理(on-device inference)和基于设备的训练(on-device training)。

苹果提供的框架和机器学习工具

Core ML 与特定领域的框架一起工作,如用于图像分析的 VisionVision 框架提供了再图像或视频上执行计算机视觉算法的高阶API封装。 Vision 可以使用苹果提供的内置模型或者自定义的 Core ML 模型对图像进行分类(classify)。

Core ML 是建立在低级别基元(lower-level primitives): BNNS加速Metal高效着色器 之上的。

iOS machine learning architecture

其他可以和 Core ML 共同工作的特定领域还包含:用于处理文本的自然语言识别和用于声音分析的音频识别

将Core ML模型集成到你的APP中

为了在 App 中集成 Core ML ,你需要一个 Core ML 格式的模型。Apple 提供了一些 预训练(就是使用少量数据训练的,效果比较一般的,译者注)的模型,可以用于图像分类等需求。如果这些模型不能满足你的要求,你可以自行去AI社区查找一番或者训练你自己的模型。

对于项目 Vibes 的一些改进,需要一个能够将图像分类的模型。可使用的模型具备不同程度的准确性,而这些模型也有不同的大小。(对于这个教程项目来说,)你可以使用提供的 SqueezeNet ,这是一个经过训练,可以识别常见物品的小型模型。

拖拽在 starterModels 中的 SqueezeNet.mlmodel 文件到你 Xcode 中已经打开的 Vibes 项目中,把它放到 Models 这个组下。

User adding a mode

选中 SqueezeNet.mlmodel ,可以再右侧看到这个模型的详细信息。

show model detail infomation

在预测(Prediction)部分列出了预期的输入和输出

  • 输入侧,期望得到一个 227 * 227 尺寸的图像
  • 输出侧有两种类型, classLabelProbs 返回一个字典,其中包含图像分类的概率, classLabel 返回概率最高的分类。

点击上方的 Model Class 区域:

Model class zone

Xcode 为模型自动生成一个文件,其中包括输入、输出和主类。主类包括用于预测的各种方法。

Vision 框架的标准工作流程是:

  1. 首先,创建 Core ML 模型(model)。
  2. 然后,创建一到多个请求(requests)。
  3. 最后,创建和运行多个请求句柄(request handler)。

现在,你已经有了一个现成的 Core ML 模型 SqueezeNet.mlmodel ,直接进行下一步,创建请求。

创建请求

CreateQuoteViewController.swift 文件头部 UIKit 后面增加需要的框架导入声明。

import CoreML
import Vision

Vision 有助于处理图像,如将其转换为所需的格式。

CreateQuoteViewController 增加如下的 property:

// 定义了一个懒加载的图像分析请求
private lazy var classificationRequest: VNCoreMLRequest = {
  do {
    // 创建模型实例
    let model = try VNCoreMLModel(for: SqueezeNet(configuration: MLModelConfiguration()).model)
    // 基于该模型实例化一个图像分析请求对象。完成处理程序后,接收分类结果并打印出来。
    let request = VNCoreMLRequest(model: model) { request, _ in
        if let classifications =
          request.results as? [VNClassificationObservation] {
          print("Classification results: \(classifications)")
        }
    }
    // 使用Vision来裁剪输入的图像以符合模型的期望
    request.imageCropAndScaleOption = .centerCrop
    return request
  } catch {
    // 通过杀死应用程序来处理模型加载错误。模型是应用程序捆绑的一部分,应该不会走到这。实际应用中,埋个点吧。
    fatalError("Failed to load Vision ML model: \(error)")
  }
}()

集成请求

将以下代码添加到 CreateQuoteViewController 私有扩展的底部。(写有 Private methods 注释的那个extension,译者注)

func classifyImage(_ image: UIImage) {
  // 获取图像的方向,并且将图像转换成CIImage格式提供给后续步骤
  guard let orientation = CGImagePropertyOrientation(rawValue: UInt32(image.imageOrientation.rawValue)) else {
    return
  }
  guard let ciImage = CIImage(image: image) else {
    fatalError("Unable to create \(CIImage.self) from \(image).")
  }
  // 在一个后台队列中启动一个异步分类请求,当句柄在外部被创建并且安排上时,这个Vision请求将被执行
  DispatchQueue.global(qos: .userInitiated).async {
    let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
    do {
      try handler.perform([self.classificationRequest])
    } catch {
      print("Failed to perform classification.\n\(error.localizedDescription)")
    }
  }
}

最后,在 imagePickerController(_:didFinishPickingMediaWithInfo:) 方法最后添加这段代码。

classifyImage(image)    // 当用户选择一张图片时,触发分类请求

重新编译、运行App,点击相机图标选择一张照片,这是看上去和之前没什么区别。

Waterfall with random quote at the bottom

但是在 Xcode 控制台应该打印了分类的相关元信息。

Raw classification results

在上面这个截图中,图像分类器给了cliff, drop, drop-off(悬崖,跌落,落差)约27.88%的置信度。修改 classificationRequest 中的打印语句以直观的数据输出这个结果。

// print("Classification results: \(classifications)")
let topClassifications = classifications.prefix(2).map {
  (confidence: $0.confidence, identifier: $0.identifier)
}
print("Top classifications: \(topClassifications)")

重新编译App并且运行,选择一张照片,控制台这时候直接输出了最高置信度的分类结果。

Pretty classification results

现在你可以使用分类器提供的预测细节来显示与图片相关的箴言了!

添加具有相关性的箴言

imagePickerController(_:didFinishPickingMediaWithInfo:) 方法中,注释掉以下代码。

if let quote = getQuote() {
  quoteTextView.text = quote.text
}

不需要使用这个方法来随机展示箴言了。接下来,你将使用 VNClassificationObservation 的结果来给App添加挑选最接近的箴言的逻辑。还是在 CreateQuoteViewController 的私有 extension 中增加这个函数。

func processClassifications(for request: VNRequest, error: Error?) {
  DispatchQueue.main.async {
    // 处理来自图像分类请求的结果
    if let classifications =
      request.results as? [VNClassificationObservation] {
      // 提取前两个预测结果
      let topClassifications = classifications.prefix(2).map {
        (confidence: $0.confidence, identifier: $0.identifier)
      }
      print("Top classifications: \(topClassifications)")
      let topIdentifiers =
        topClassifications.map {$0.identifier.lowercased() }
      // 将预测结果传入getQuote(for:)获得一个相关的箴言
      if let quote = self.getQuote(for: topIdentifiers) {
        self.quoteTextView.text = quote.text
      }
    }
  }
}

因为箴言的视图会进行UI更新,所以这个方法必须在主线程执行。

最后,在 classificationRequest 方法中将 request 的初始化方法修改如下。

let request = VNCoreMLRequest(model: model) { [weak self] request, error in
    guard let self = self else {
      return
  }
  // 当预测结束时,调用方法更新箴言
  self.processClassifications(for: request, error: error)
}

重新编译运行,选择一张含有柠檬或者柠檬树的照片(模拟器自带,译者注)。这时候显示的箴言应该和柠檬相关了,而不是随机展示的箴言。

Close up of tree with related quote

观察在Xcode中分类器的日志,

Classification results that match the image above

你可以多尝试几次验证结果的一致性。

Great Stuff! 你已经学会了如何使用 Core ML 进行设备上的模型推理。:]

Happy, cheering iPhone with medals

在设备上对模型进行个性化处理

通过最新的 Core ML 3 ,可以在运行期间在设备上对一个可更新的模型进行微调。这意味着你可以为每个用户提供个性化的体验。

基于设备的个性化处理是 FaceID 背后的逻辑。Apple 将一个通用的模型下发到设备上,识别一般的人脸,在 FaceID 设置过程中,每个用户可以对这个模型进行微调以识别他们自己的脸。

将这一更新的模型返回给 Apple,再部署给其他用户,是没有意义的。所以这个功能也凸显了基于设备个性化处理隐私方面的优势。

可更新的模型是一个标记为“可更新”的 Core ML 模型,你也可以将你自己训练的模型定义为可更新的。

K最近邻分类(k-NN)算法

接下来,你会使用一个可更新的绘图分类器来改进 Vibes 这个项目。这个分类器是基于k-NN(k-Nearest Neighbors)的。这时黑人问号脸就出来了, k-NN 是什么?

k-NN 算法,简单的说,就是“同性相吸”。

Bunch of sunglass smiley faces and one awkward rectangle

它通过比较特征向量 (feature vectors) 来达到想要的结果,一个特征向量包含描述一个物体特征的关键信息,比如使用特征向量R、G、B来表示RGB颜色。

比较特征向量之间的距离是查看两个物体是否相似的简单方法,k-NN 对临接的K个邻居进行分类。

下面的例子展示了一个已经按照正方形和圆形形状进行分类分布的场景,如果你想识别神秘的红色属于哪一组。

k-NN plot with groups of blue rectangles and green circles

根据距离画个圈,可以看到 k = 3 的时候可以预测到这个新图形是一个正方形。

k-NN classification graph with three blue rectangles circled

k-NN 模型简单而又迅速,不需要很多数据就可以训练。但是随着样例数据越多,它的性能也会变得越慢。

k-NNCore ML 支持训练的模型类型之一。在接下来的示例中, Vibes 这个项目将使用可更新的绘图分类器完成:

  1. 一个作为特征提取器的神经网络,它知道如何识别图形。你需要为 k-NN 模型提取特征。
  2. 一个用于基于设备绘图个性化处理的 k-NN 模型
Half circle, arrow to Static web of dots, arrow to unadaptable classifier, arrow to laughing emoji

接下来,在 Vibes 这个项目中,用户可以添加一个一次性画3个表情符号的快捷方式。你将会以表情符号作为标签,以绘图作为实例来训练模型。

设置绘图训练流程

首先,准备好响应用户在屏幕上的点击事件,用来训练你的模型:

  1. 增加一个界面用于选择表情符号。
  2. 增加点击保存的动作。
  3. stickerLabel 中删除 UIPanGestureRecognizer 手势。

打开 AddStickerViewController.swift 文件,在 collectionView(_:didSelectItemAt:) 中注释掉原本 performSegue(withIdentifier:sender:) 这一行代码,并且替换为:

performSegue(withIdentifier: "AddShortcutSegue", sender: self)

当用户点击一个表情符号时,会前往一个新的页面。

接着打开 AddShortcutViewController.swift 文件在 savePressed(_:) 方法中添加以下代码:

print("Training data ready for label: \(selectedEmoji ?? "")")
// 当用户点击保存按钮时,回到首页
performSegue(
  withIdentifier: "AddShortcutUnwindSegue",
  sender: self)

最后,打开文件 CreateQuoteViewController.swift 并且注释掉以下代码:

stickerLabel.isUserInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer(
  target: self,
  action: #selector(handlePanGesture(_:)))
stickerLabel.addGestureRecognizer(panGestureRecognizer)

通过注释这段代码,禁止用户移动表情符号。这个功能只有用户在无法控制贴纸位置的时候才有用。

重新编译运行 App,选择一张图片,点击贴纸图标选择一个表情符号。你会看到你选择的表情符号和三个用于绘图的画布。

Screen with Add a Shortcut header, laughing emoji and three blank rectangles

现在在画布上绘制3个相似的图形,全部完成后 Save 按钮才会被设置为可以点击的状态。

译者注:如果你发现在模拟器上无法绘图,是因为 Vibes 这个项目在这里用了 PKCanvasView 作为画布,可以在 DrawingView.swift 文件中 setupPencilKitCanvas 方法在设置 canvasView 属性的时候,添加以下代码来允许模拟器模拟Apple pencil绘图。

if #available(iOS 14.0, *) {
  canvasView.drawingPolicy = .anyInput
} else {
  canvasView.allowsFingerDrawing = true
}
Same screen as before but with hand-drawn half circles each rectangle

然后点击 Save 按钮,你会看到在 Xcode 的控制台中打印了以下日志:

Console logs selected emoji

接下来你就可以将工作聚焦到如何使用保存的快捷方式上。

添加快捷方式绘制视图

现在是时候通过以下步骤来实现在图像上进行直接的绘制了:

  1. 首先声明一个 DrawingView
  2. 接下来在主视图中添加绘制视图。
  3. 然后,从 viewDidLoad() 中调用 addCanvasForDrawing
  4. 最后,在完成图像选择后清除画布。

打开 CreateQuoteViewController.swift 文件,在 @IBOutlet 定义区域后面添加以下声明:

var drawingView: DrawingView!   // 用户绘制快捷方式的画布视图

接下来添加以下代码以实现 addCanvasForDrawing() 方法:

// 创建绘图视图示例
drawingView = DrawingView(frame: stickerView.bounds)
// 添加到主视图
view.addSubview(drawingView)
// 添加约束防止和贴纸视图重叠
drawingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
  drawingView.topAnchor.constraint(equalTo: stickerView.topAnchor),
  drawingView.leftAnchor.constraint(equalTo: stickerView.leftAnchor),
  drawingView.rightAnchor.constraint(equalTo: stickerView.rightAnchor),
  drawingView.bottomAnchor.constraint(equalTo: stickerView.bottomAnchor)
])

然后在 viewDidLoad() 末尾添加以下内容:

addCanvasForDrawing()
drawingView.isHidden = true
// 添加绘图视图,初始隐藏

现在在 imagePickerController(_:didFinishPickingMediaWithInfo:) 方法中,在 addStickerButtonisEnabled 属性在被设置为 true 之后添加以下代码。

// 清除画布,隐藏绘图视图以便表情符号的贴纸可以正确展示
drawingView.clearCanvas()
drawingView.isHidden = false

编译并且运行 App,选择一张照片,使用鼠标或者手指,验证下载图片上可以绘制图形。

Waterfall with quote and drawings added

一个小目标搞定!我们继续。

进行模型预测

starterModels 文件夹中拖拽 UpdatableDrawingClassifier.mlmodel 到 Xcode 项目视图中 Models 组里。

Add updatable model in Xcode

在项目导航窗格里选中 UpdatableDrawingClassifier.mlmodel 文件,在 Updates 这个标签里列出了模型在训练期间期望的两个输入,一个代表绘制的图形,另一个代表表情符号的标签。

Updatable model details in Xcode

Predictions 部分展示了输入和输出,格式和训练期间使用的格式一致,输出项表示表情符号的标签。

在Xcode的项目导航窗格中选择 Models 文件夹,然后:

  1. 点击 File ▸ New ▸ File…
  2. 在对话框中选择 iOS ▸ Source ▸ Swift File,点击 Next
  3. 将创建的文件命名为 UpdatableModel.swift 点击 Create

将文件头部的 import Foundation 替换为:

import CoreML   // 引入机器学习框架

接下来在文件末尾添加以下代码。

extension UpdatableDrawingClassifier {
  // 确保图像与模型所期望的一致
  var imageConstraint: MLImageConstraint {
    return model.modelDescription
      .inputDescriptionsByName["drawing"]!
      .imageConstraint!
  }

  // 用绘图的CVPixelBuffer调用模型的预测方法。
  // 返回预测的表情符号标签,如果没有匹配的则返回nil
  func predictLabelFor(_ value: MLFeatureValue) -> String? {
    guard
      let pixelBuffer = value.imageBufferValue,
      let prediction = try? prediction(drawing: pixelBuffer).label
      else {
         return nil
    }
    if prediction == "unknown" {
      print("No prediction found")
      return nil
    }
    return prediction
  }
}

更新模型

UpdatableModel.swift 文件的 import 区域后面添加以下代码:

// 映射可更新的模型
struct UpdatableModel {
  private static var updatedDrawingClassifier: UpdatableDrawingClassifier?
  private static let appDirectory = FileManager.default.urls(
    for: .applicationSupportDirectory,
    in: .userDomainMask).first!
  // 指向原始编译模型
  private static let defaultModelURL =
  UpdatableDrawingClassifier.urlOfModelInThisBundle
  // 保存模型的位置
  private static var updatedModelURL =
  appDirectory.appendingPathComponent("personalized.mlmodelc")
  private static var tempUpdatedModelURL =
  appDirectory.appendingPathComponent("personalized_tmp.mlmodelc")

  private init() { }

  static var imageConstraint: MLImageConstraint {
    guard let model = try? updatedDrawingClassifier ?? UpdatableDrawingClassifier(configuration: MLModelConfiguration()) else {
      fatalError("init UpdatableDrawingClassifier error")
    }
    return model.imageConstraint
  }
}

TIPS: Core ML使用一个扩展名为.mlmodelc的编译模型文件,它实际上是一个文件夹。

将模型加载到内存

接下来,在上面这个 struct 定义后面添加以下代码:

private extension UpdatableModel {
  // 加载模型
  static func loadModel() {
    let fileManager = FileManager.default
    if !fileManager.fileExists(atPath: updatedModelURL.path) {
      do {
        let updatedModelParentURL =
          updatedModelURL.deletingLastPathComponent()
        try fileManager.createDirectory(
          at: updatedModelParentURL,
          withIntermediateDirectories: true,
          attributes: nil)
        let toTemp = updatedModelParentURL
          .appendingPathComponent(defaultModelURL.lastPathComponent)
        try fileManager.copyItem(
          at: defaultModelURL,
          to: toTemp)
        try fileManager.moveItem(
          at: toTemp,
          to: updatedModelURL)
      } catch {
        print("Error: \(error)")
        return
      }
    }
    guard let model = try? UpdatableDrawingClassifier(
      contentsOf: updatedModelURL) else {
      return
    }
    // 模型加载到内存
    updatedDrawingClassifier = model
  }
}

以上代码将已经更新、编译完毕的模型加载到内存。接下来在 struct 定义后面添加这个公开的扩展。

extension UpdatableModel {
  static func predictLabelFor(_ value: MLFeatureValue) -> String? {
    loadModel()
    return updatedDrawingClassifier?.predictLabelFor(value)
  }
}

predict 方法将模型加载到内存中并且在你添加的扩展方法中调用预测方法。

接下来打开 Drawing.swift 文件在 import PencilKit 后面添加以下导入代码。

import CoreML

开始准备预测输入的信息。

预测准备

Core ML 希望开发人员将预测的输入数据包装在一个 MLFeatureValue 对象中,这个对象包含数据类型和数据值。

Drawing.swift 文件已经定义的 struct Drawing 尾部添加以下代码:

var featureValue: MLFeatureValue {
  let imageConstraint = UpdatableModel.imageConstraint

  let preparedImage = whiteTintedImage

  let imageFeatureValue =
    try? MLFeatureValue(cgImage: preparedImage, constraint: imageConstraint)
  return imageFeatureValue!
}

这段代码定义了一个计算属性,用于设置绘图的特征对象,这个特征对象是基于一个全白的图像和图像的相关约束。

现在你已经准备好了输入数据,可以专注于触发预测动作了。

首先,打开 CreateQuoteViewController.swift ,在文件末尾添加 DrawingViewDelegate 扩展。

extension CreateQuoteViewController: DrawingViewDelegate {
  func drawingDidChange(_ drawingView: DrawingView) {
    // 绘图的边界,防止越界
    let drawingRect = drawingView.boundingSquare()
    // 绘图实例
    let drawing = Drawing(
      drawing: drawingView.canvasView.drawing,
      rect: drawingRect)
    // 为绘图预测输入创建特征值
    let imageFeatureValue = drawing.featureValue
    // 进行预测,以获得与该绘制图形相对应的表情符号
    let drawingLabel =
      UpdatableModel.predictLabelFor(imageFeatureValue)
    // 更新主队列中的视图,清除画布并将预测的表情符号添加到主视图中
    DispatchQueue.main.async {
      drawingView.clearCanvas()
      guard let emoji = drawingLabel else {
        return
      }
      self.addStickerToCanvas(emoji, at: drawingRect)
    }
  }
}

回忆一下,上面的步骤中你已经添加了一个 DrawingView 来绘制表情符号的快捷方式,在以上代码中遵循了 DrawingViewDelegate ,在每次绘图发生变化时,这部分代码就会得到响应。

接着在 imagePickerController(_:didFinishPickingMediaWithInfo:) 删除以下重置画布的操作,因为在上面进行预测的方法中已经清理了画布。

drawingView.clearCanvas()

测试预测

接下来在 addCanvasForDrawing() 方法中添加 drawingView 的代理。

drawingView.delegate = self

这使得视图控制器成为绘图视图代理。

编译并运行该应用程序,选择一张照片。在画布上绘图,验证绘图完成后,画布是否被重置,并在控制台中记录了以下内容。

Log results shows no prediction

那是意料之中的事。你还没有添加表情贴纸的快捷方式呢~

现在来到添加表情贴纸快捷方式的流程。在你回到所选照片的视图后,绘制同样的快捷方式。

Close-up of dandelion with quote

哎呀,贴纸还是没有被添加! 你可以查看控制台日志来查看问题。

Sticker still not added

经过一番折腾之后,你可能会注意到训练的模型对所要添加的贴纸完全无法理解。是时候解决这个问题了!

更新模型

更新模型需要创建一个 MLUpdateTask 。更新任务的初始化方法需要编译后的模型文件、续联数据和一个完成后的回调句柄。一般来说,把更新后的模型保存到磁盘并且重新加载,新的预测就会使用最新 的模型数据。

首先你需要根据快捷方式的绘制图形准备训练数据。

回顾一下,之前通过传入一个 MLFeatureProvider 特征对象输入来进行模型预测流程,同样的,你也可以传入一个 MLFeatureProvider 特征对象输入来训练一个模型。另外你也可以传入一个包含多个特 征的 MLBatchProvider 来进行批量预测或者批量训练。

要进行以上的操作,首先,打开 DrawingDataStore.swift 文件替换 import Foundation 为:

import CoreML

译者注:导入CoreML 的时候,内部已经导入过Foundation框架,所以这里直接替换掉就可以了

然后在扩展的末尾添加以下方法:

func prepareTrainingData() throws -> MLBatchProvider {
  // 初始化一个空的 MLFeatureProvider 数组
  var featureProviders: [MLFeatureProvider] = []
  // 定义模型训练输入的名称
  let inputName = "drawing"
  let outputName = "label"
  // 循环浏览数据存储中的图形数据
  for drawing in drawings {
    if let drawing = drawing {
      // 将绘图训练输入包在一个特征值中
      let inputValue = drawing.featureValue
      // 将emoji训练输入包在一个特征值中
      let outputValue = MLFeatureValue(string: emoji)
      // 为训练输入创建一个MLFeatureValue集合。这是一个训练输入名称和特征值的字典。
      let dataPointFeatures: [String: MLFeatureValue] =
        [inputName: inputValue,
        outputName: outputValue]
      // 为 MLFeatureValue 集合创建一个 MLFeatureProvider,并将其追加到数组中
      if let provider =
        try? MLDictionaryFeatureProvider(
          dictionary: dataPointFeatures) {
        featureProviders.append(provider)
      }
    }
  }
  // 最后,从MLFeatureProvider数组中创建一个批处理对象(MLArrayBatchProvider)
  return MLArrayBatchProvider(array: featureProviders)
}

现在,打开 UpdatableModel.swift 文件,在 UpdatableDrawingClassifier 扩展末尾添加以下代码:

static func updateModel(
  at url: URL,
  with trainingData: MLBatchProvider,
  completionHandler: @escaping (MLUpdateContext) -> Void
) {
  do {
    let updateTask = try MLUpdateTask(
      forModelAt: url,
      trainingData: trainingData,
      configuration: nil,
      completionHandler: completionHandler)
    updateTask.resume()
  } catch {
    print("Couldn't create an MLUpdateTask.")
  }
}

以上代码使用编译后的模型URL创建 MLUpdateTask ,还传入了一个带有训练数据批处理 MLBatchProvider 。对这个任务调用 resume() 开始训练,当训练结束时, completionHandler 被调用。

保存模型

接下来,在 UpdatableModel 私有扩展(private extension)末尾添加以下代码:

static func saveUpdatedModel(_ updateContext: MLUpdateContext) {
  // 首先,从内存中获取更新的模型。这与原始模型不一样。
  let updatedModel = updateContext.model
  let fileManager = FileManager.default
  do {
    // 然后,创建一个中间文件夹来保存更新的模型。
    try fileManager.createDirectory(
        at: tempUpdatedModelURL,
        withIntermediateDirectories: true,
        attributes: nil)
    // 把更新的模型写到一个临时文件夹中
    try updatedModel.write(to: tempUpdatedModelURL)
    // 替换模型文件夹的内容
    // 直接覆盖现有的mlmodelc文件夹会出现错误。
    // 解决方案是保存到一个中间文件夹,然后把内容复制过来。
    _ = try fileManager.replaceItemAt(
      updatedModelURL,
      withItemAt: tempUpdatedModelURL)
    print("Updated model saved to:\n\t\(updatedModelURL)")
  } catch let error {
    print("Could not save updated model to the file system: \(error)")
    return
  }
}

这个辅助类完成了保存更新模型的任务,它接收了一个 MLUpdateContext ,其中包含了训练相关的有用信息。

执行更新后的模型

UpdatableModel 的公开扩展(public extension)末尾添加以下代码:

static func updateWith(
  trainingData: MLBatchProvider,
  completionHandler: @escaping () -> Void
) {
  loadModel()
  UpdatableDrawingClassifier.updateModel(
    at: updatedModelURL,
    with: trainingData) { context in
      saveUpdatedModel(context)
      DispatchQueue.main.async { completionHandler() }
  }
}

以上代码将模型加载到内存,然后调用私有扩展中定义的更新方法,完成处理流程后保存更新后的模型,然后再运行这个流程中的成功回调句柄。

然后,打开 AddShortcutViewController.swift 文件,替换 savePressed(_:) 方法的实现:

do {
  let trainingData = try drawingDataStore.prepareTrainingData()
  DispatchQueue.global(qos: .userInitiated).async {
    UpdatableModel.updateWith(trainingData: trainingData) {
      DispatchQueue.main.async {
        self.performSegue(
          withIdentifier: "AddShortcutUnwindSegue",
          sender: self)
      }
    }
  }
} catch {
  print("Error updating model", error)
}

在这里,你已经吧一切都混合在一起。设置好训练数据后,启动了一个后台线程来更新模型,更新完毕后调用 AddShortcutUnwindSegue 跳转会主视图。

编译并且运行App,通过以下步骤来创建一个快捷方式。

Add a shortcut screen with heart eyes emoji, there gray rectangles with hearts drawn in them

当你点击 Save 按钮时,观察以下 Xcode 的控制台输出。

Model update logged

在选定的照片上绘制同样的快捷方式团,并验证是否显示了正确的表情符号。

Four screens: flower close up with quote, add a shortcut with hearts, flower close up with heart, flower closeup with heart eye emoji

恭喜你,获得了机器学习忍者称号!

Machine Learning ninja warrior

接下来呢?

你可以看下下载项目中已经完成的项目(final 文件夹里,但是Xode 12.5可能跑不起来。。译者注)。

查看iOS中的 机器学习视频课程 ,了解更多关于如何使用 Create MLTuri Create 来训练自己的模型。Beginning Machine Learning with Keras & Core ML 指导你如何训练一个神经网络并将其转换为Core ML。

Create ML应用可以让你建立、训练和部署机器学习模型,不需要机器学习的专业知识。你还可以查看WWDC 2019的官方视频,What’s New in Machine LearningTraining Object Detection Models in Create ML

客户端跨端的另一种尝试:Go mobile

众所周知, flutterGoogle 在 2018 年发布,用于原生跨端(当然 2.0 可以做更多的事情~)框架,基于 Dart 语言开发。很多人都很奇怪,因为在这段时间内,2012年正式发布的 Go 一直是 Google 主推,为什么 flutter 这个开源框架会使用名不见经传的 Dart 作为开发语言呢?是 Go 不能进行跨端开发么?

其实早在2012年, Google 就在 Github 上开源了基于 Go 的跨端开源解决方案 golang/mobile 并且持续维护。但是对于这个已经开源了9年的“老项目”, Google 官方却在 Readme 中仍然标注这个方案仍然是一个实验性的项目。对于这个项目笔者甚是感兴趣,近期便来调研一番。

编译到 Android

对于如何与移动应用集成, golang/mobile 已经提供了2种方式:

  1. all-in,即整个App都使用 golang 构建
  2. SDK,部分代码使用 golang 构建,App可以通过集成 framework 或者 aar 进行

我们先来尝试 all-in 的方式,笔者使用的 macOS 系统,在已经安装好 golang 运行时的基础上,只要执行以下命令就可以在命令行使用 gomobile 了。

$ go get golang.org/x/mobile/cmd/gomobile

由于不需要考虑签名等繁琐事项,我们使用 golang/mobile 构建一个 apk 试试。

官方已经提供了简单的Demo位于 https://golang.org/x/mobile/example 可以直接 git clone 下来,进入 example 目录后,执行以下命令下载依赖。

$ go get -d golang.org/x/mobile/example/basic

随后,使用 gomobile build 直接将示例代码打包成一个 apk

$ gomobile build -target=android golang.org/x/mobile/example/basic

但是执行这一步的时候,终端缺直接报错了。

gomobile: ~/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-nm /var/folders/09/4vfx52xs0r5_0dl7s5g8qchr0000gn/T/gomobile-work-745714058/lib/armeabi-v7a/libbasic.so: fork/exec ~/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-nm: no such file or directory

进入 ~/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/ 这个目录看了下确实不存在 arm-linux-androideabi-nm 这个文件。从文件路径来看 gomobile 应该是通过 NDK 的方式进行编译,在参考了相关的ISSUE后,笔者下载了 android-ndk-r12b-linux-x86_64.zip 这个版本的NDK并且将 arm-linux-androideabi-nm 复制到了报错的路径,继续执行 gomobile build 却发现还是报错。

gomobile: ~/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-nm /var/folders/09/4vfx52xs0r5_0dl7s5g8qchr0000gn/T/gomobile-work-553399005/lib/arm64-v8a/libbasic.so: fork/exec ~/Library/Android/sdk/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-nm: no such file or directory

原来是缺失另一个CPU架构的可执行文件,在 android-ndk-r12b-linux-x86_64中找到同名文件,直接复制到报错路径后就可以了。但是执行还是报错,提示缺少 i686-linux-android-nm

在经过了反复折腾,查阅了几个小时资料,对比了几个NDK Bundle后,终于发现问题所在。由于笔者使用的系统是 macOS Big Sur 11.2.1 ,Android Studio只有Beta版可以很好的支持这个操作系统。但是 Android Studio 4.2 PreviewSDK Manager 中默认下载的NDK(版本号 23.0.7196353-beta2)居然不是完整版,缺少了很多包括 nmobjcopy 等重要的可执行文件,所以只需要在 SDK Manager 勾选 Show Package Details ,展开 NDK(Side by side) 后选择稳定版 21.4.7075529 安装即可。

image-20210406224426421

安装后,如果你电脑上NDK的环境变量关联了版本,需要记得在 ~/.bash_profile (或者 ~/.zshrc ,看你使用的shell)中修改NDK的版本路径,比如:

export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/21.4.7075529
# 修改后执行 source ~/.bash_profile

再次执行 gomobile build 后便可以在目录中看到已经编译生成了 basic.apk 。 将这个 apk 拖入AVD,运行 Basic 这个App就可以看到官方Demo的效果啦~

image-20210406224838552

重构 02:开始诊疗

Translated from:Refactoring

大多数代码,都具备“屎山”的特征。

代码的味道

  • 纳尼?代码怎么会有味道?
  • 虽然闻不出来…但是我敢打赌它肯定会发臭!

诊断:臃肿

臃肿的代码,包括臃肿的函数和类等等,太过臃肿的代码导致程序几乎无法运行。通常臃肿的问题不会马上就出行,而是随着时间推移,随着程序的发展,而逐渐积累(特别是没有人去关心这个问题时)。

病症:过长的函数

症状

一个函数包含太多的代码行。一般来说,任何超过十行的函数都应该让你产生不安的感觉。

long-method-01

产生原因

就像 Hotel California (译者注:不是很清楚这个梗,应该是这个),某些函数总是在一直添加东西,但没有任何逻辑被拆分或删除。由于写代码比读代码容易,这种“臭味”一直没有被注意到,直到函数变成一个丑陋、巨大的怪兽。

从开发人员的心态来说,创建一个新函数往往比在原有函数中添加逻辑更难。“只是再加两行代码而已,没有必要给这段逻辑单独放一个函数里…”。带着这种想法,我们经常在函数中塞入一行又一行的代码,最终产生了一团乱七八糟,像意大利面条一样的东西。

解决方案

一个经验法则是,如果你觉得需要在一个函数里,针对一段代码需要写注释,那你应该把这段代码放到一个新的函数中。如果是一定需要写注释的代码,哪怕是一行,也应该拆分出一个单独的函数。如果函数有一个描述性的名字,那后面的开发人员就不需要看代码来了解内部的运作。

long-method-02

小结

  • 在所有面向对象的项目中,函数越短的类寿命最长。一个函数或者函数越长就越难理解和维护。
  • 此外,长函数为不必要的重复代码提供的完美的藏身之处。
long-method-03

优化后会影响性能吗?

很多人说,增加太多函数会影响性能?但是在几乎所有试验中,其带来的性能损耗微乎其微,不值得担心。

而且通过以上措施,我们获得了清晰易懂的代码,在优化过程中,更有可能找到真正有效的函数来重组代码,获得真正的性能提升。

病症:过长的类

症状

一个类包含太多字段/函数/代码行。

large-class-01

产生原因

一般来说,类一开始被创建的时候都会比较小,但是一旦随着岁月流逝,这个类就会变得越来越大,功能也会越来越复杂。

和过长的函数一样,比起新创建一个类,程序员们更喜欢将新功能放在现有的类中。

large-class-02

解决方案

当一个类包含了大量的函数时,需要进行分析并且通过以下手段尝试将它们拆分。

  • 如果类中包含了可以独立的逻辑或者组件时,可以直接 拆分类
  • 如果类中包含的逻辑可以差异化实现,或者很少运行到的逻辑,可以尝试 拆分子类
  • 如果类中的函数和逻辑可以完全抽象,调用时再进行实现,可以尝试 拆分接口
  • 对于复杂的UI界面,比较好的函数是将模型数据和逻辑拆分到单独的对象中。这是可能会在2个地方存储一些数据的副本,并且保持数据的一致性。重复数据观察模式 提供了一种处理这种情况的方案。
large-class-03

重构了之后…

  • 重构这些大型类后,可以显著降低开发人员的心智负担。
  • 通常拆分大类后,可以避免很多重复的代码和功能。

病症:基础类型的滥用

症状

  • 一些简单的事物(如货币、范围、电话号码特殊字符等)使用基础类型而不是使用对象
  • 使用常量来编码信息,比如定义常量 USER_ADMIN_ROLE = 1来描述具备管理员权限的用户
  • 使用字符串常量作为数据类数组的字段名
primitive-obsession-01

产生原因

和其他问题一样,基础数据的滥用也是一时的懈怠。“只是想要一个东西放临时的数据!”程序员产生这种想法的时候,都是觉得比起创建一个全新的类,直接创建一个基础类型更方便。当每一次遇到相同的情况时,都会有这样偷懒的想法,导致我们最后创建了一个又一个的基础类型存放数据,而容纳这些基础类型属性的类也逐渐变得越来越大。

基础类型通常用于“仿冒”类型。比如,可以使用一组数字或字符串(而不是单独的数据类型)来构成某些实体的允许值列表。然后,通过常量为这些特定的数字和字符串提供易于理解的名称,这就是为什么它们被滥用的原因。

另外一个不正确使用基础类型的方式是使用基础类型“仿冒”类的字段。比如类中包含了大量不同的数据数组,在类中指定的字符串常量用于获取这些数据数组的索引。

解决方案

primitive-obsession-02

重构了之后…

  • 使用对象替换基础类型,可以使代码更加灵活。
  • 重构以后代码更容易理解,更加具备组织性。对特定数据的操作都在同一个地方而不是分散在各处。在阅读代码时不会出现难以猜测的奇怪常量,或者为什么有奇怪的常量出现在数组中。
  • 更容易找到重复的代码。

病症:大串的函数形参

症状

一个函数的参数超过三个或四个。

产生原因

把几种类型算法合并到一个函数中后,可能会给这个函数带来一长串的形参。大串的形参可能是为了控制函数中哪些逻辑被执行、被如何执行。

大串的形参也可能是拆分类的副产品。例如原始函数中创建特定对象的代码转移给了调用方(译者注:这样原始函数就会更加的灵活),调用方创建对象后通过参数传递,这样这个特定对象就和原始函数解耦了。但是如果调用方传递了多个不同类型的对象,每个对象都需要自己特有的参数,这样原始函数可能就需要大串的形参了。

函数中大串的形参会导致理解困难,当形参越来越长,函数也就越来越难以使用。通常也可以使用相关对象来代替大串形参,如果这个对象不能包含所有需要的信息,通过会创建额外的对象再作为函数参数。

解决方案

  • 检查传递参数的值。如果有些参数只是另一个对象的调用结果,那么可以 使用函数调用替换参数。这个对象可以放在自己的类字段中,也可以作为函数参数传递。
  • 不要将从另一个对象接受到的一组数据直接作为参数传递,而是将对象本身传递给函数,参见 保留整个对象
  • 如果有几个不相干的数据元素,有时可以 引入对象参数 ,将他们合并为一个参数对象。
long-parameter-list-02

重构了之后…

  • 精简、易读的代码。
  • 可能会发现以前没注意到的重复代码。

病症:重复代码簇

特征

有时相同的代码簇出现在程序的不同部分,比如连接数据库的参数。这些代码簇应该被整理成类。

data-clumps-01

产生原因

通常这些重复代码是由于架构不完善或者“Ctrl-C + Ctrl-V编程”造成的。

如果要确认一些代码是不是重复代码簇,只要删除其中一个数据值,看看其他值是否还有意义。如果不是这种情况,则表明这组变量应合并到一个对象中,这是一个好兆头。

解决方案

  • 如果重复的数据由一个类的字段组成,使用 拆分类 将字段移到它们自己的类中。
  • 如果在函数的参数中传递了相同的数据块,使用 引入对象参数 将它们设置为一个类。
  • 如果只是部分数据被传递给其他函数,请考虑将整个数据对象传递给函数,而不仅仅是单个字段。保留整个对象 将帮助解决这个问题。
  • 检查使用这些代码的字段,有时将这些代码转移到数据类中可能是个好主意。
data-clumps-02

重构之后…

  • 代码更有组织性易于理解。对特定数据的操作现在被收集在一个地方,而不是杂乱无章地分布在整个项目中。
  • 减少代码体积。
data-clumps-03

特例:什么时候忽略这个问题?

在函数参数中传递整个对象,而不是只传递它的值(基础类型),可能会在两个类之间产生意料之外的依赖关系。

重构 01:前言

Translated from:Refactoring

重构是一个系统化的过程,在不创建新功能的情况下改进代码,可以将一团糟的代码转化为干净的代码和简单的设计。

重构工作包含:简化代码

重构的主要目的是对抗技术债务。它将一团乱麻转化为干净的代码和简单的设计。

很好!但什么是干净的代码呢?下面是它的一些特点。

1. 干净的代码对于其他程序员来说是显而易见的。

这里说的不是超级复杂的算法。糟糕的变量命名,臃肿的类和方法,神奇的数字–你能想到的–所有这些都会让代码变得混乱和难以控制。

2. 干净的代码不包含重复的内容。

每次要对重复的代码进行修改时,都要记得对每个相同的部分进行同样的修改。这增加了记忆负担,同时也减缓了开发进度。(译者:代码质量也很堪忧)

3. 干净的代码包含了最少数量的类和其他可移植的组件。

代码少了,脑子里要装的东西就少了。更少的代码就是更少的维护。代码少就是bug少。代码就是负担,要短小精悍。

4. 干净的代码通过所有测试。

当你的测试只有95%通过时,你就知道你的代码很脏。当你的测试覆盖率为0%的时候,你就知道你完蛋了。

5. 干净的代码更容易维护,成本更低!

重构工作包含:解决技术性债务

每个人都会尽自己最大的努力,从零开始写出优秀的代码。可能没有一个程序员会故意写出不干净的代码来损害项目。但是,干净的代码在什么时候会变得不干净呢?

关于不干净代码的 “技术债务 “的比喻最初是由 Ward Cunningham 提出的。

如果你从银行获得贷款,这可以让你更愉快的进行购物。当然,你要付出额外的代价–不仅要偿还本金,还要偿还贷款的额外利息。最终,你甚至可以积累茫茫多的利息,以至于利息金额超过了你的总收入,让你无法全额还款。

同样的事情也会发生在代码上。你可以暂时不为新功能写测试而加快进度,但这将使你每天的进度逐渐变慢,直到你最终通过写测试来还清债务。

技术性债务的原因

1. 业务压力

有时,业务方可能会迫使你在功能完全完成之前就将其发布。这种情况下,为了掩盖未完成的功能,代码中会充斥着补丁和不成熟的代码。

2. 对技术性债务的后果缺乏了解。

有时,你的老板可能不理解技术债务有 “利息”,因为随着债务的积累,它会减缓开发的速度。这可能会使团队很难将时间用于重构,因为管理层没有看到它的价值。

3. 组件严重耦合。

当项目类似于整体而不是单个模块的产品时。 在这种情况下,对项目某一部分的任何更改都会影响其他部分。 团队开发变得更加困难,因为开发工作都将耦合在一起,可能都无法进行独立的开发。

4. 缺乏测试。

缺乏及时测试的代码虽然速度快,但是对项目充满了风险和问题。在最糟糕的情况下,这些改动在没有任何事先测试的情况下就被部署和运行到生产中。其后果可能是灾难性的。例如,一个看似无害的热修复程序可能会向成千上万的客户发送一封奇怪的测试电子邮件。甚至更坏的情况,缺陷会重置或破坏整个数据库。

5. 缺乏文档。

这就减缓了项目引进新人的速度,如果关键开发人员离开,就会使项目发展停滞不前。

6. 团队成员之间缺乏互动。

如果知识库没有在整个公司广泛运用,人民最终会在对项目的流程和信息的理解上因信息不同步导致问题发生。如果初级开发人员没有得到导师的正确培训,这种情况会更加严重。

7. 在长分支上持续开发。

这可能会导致技术债务的积累,当分支合并时,技术债务就会增加。在一根分支上单独提交的代码越多,合并时产生的技术债务总额就越大。

8. 不断推迟重构的时间。

项目的需求是不断变化的,在某些时候,可能会发现部分代码已经过时,已经变得很繁琐,必须重新设计以满足新的需求。

另一方面,项目的程序员每天都在过时的部分中不断编写新的代码。因此,重构的时间拖得越长,将来需要重构的代码依赖就越多。(译者注:大概是指数级吧)

9. 没有遵循的编码规范。

当每个在项目中工作的人都按照自己认为合适的方式写代码时,就会出现这种情况。

10. 能力问题。

参与的开发 too simple too naive,完全不知道怎么写代码。

重构的时机

“三次”定律…

  1. 当你写第一次代码时,直接写就是了。
  2. 当你写第二遍,发现代码有点相似,仍然可以直接写。
  3. 同样的逻辑写第三次是,可以开始重构了!

当添加功能时…

  1. 重构可以帮助你理解别人的代码。如果你不得不处理别人的脏代码,试着先重构一下。干净的代码更清晰易懂,同时,赠人玫瑰,手有余香。
  2. 重构让你更容易添加新功能。在干净的代码中进行修改要容易得多。

当修复缺陷时…

代码中的bug就像现实生活中的蟑螂一样:它们生活在代码中最黑暗、最肮脏的地方。清理你的代码,bug就会自己跑出来。

老板们很欣赏承担主动重构任务的开发人员,因为它省去了以后专门安排人力的重构任务。Happy bosses make happy programmers!

当代码审查时…

代码审查可能是发布之前整理代码的最后机会。最好与原作者结伴进行这种审查。这样你可以快速修复简单的问题,并评估修复较难问题的时间。

如何进行重构

重构应该小步快进,每一个改动都会让现有的代码变得稍微好一点,同时还能让程序保持工作状态。

用正确的方式进行重构的检查表

√ 代码应当越来越简洁

如果重构后代码仍然不干净… 抱歉,但你刚刚可能浪费了一个小时的时间。试着找出为什么会发生这种情况。

当你合并一堆重构导致的小修改点时,经常会发生这种情况。当你开发压力非常大时,会感到抓狂。

但在处理极度草率的代码时,也会出现这种情况。无论你如何改进,代码整体上仍然是一场灾难。(译者注:屎山无论怎么整理终究还是屎山。)

这是你应该考虑彻底重写这部分代码。但在这之前,你应该已经写好了测试,并预留一大块开发时间。否则,你还是会回到第一段的情形。

√ 不应该在重构过程中添加新功能

不要把重构和新功能的直接开发混合在一起。将这些过程分开,不要同时在一个commit里面既包含重构的代码又包含新功能。

√ 重构后所有现有的测试都必须通过。

有两种情况下,重构后的测试用例会崩坏:

  1. 你在重构过程中犯了错误。 这个就不多说了:继续修正错误。
  2. 你的测试用例太差了。 例如,你在测试类的私有方法。在这种情况下,测试是罪魁祸首。你可以重构测试本身,或者编写一套全新的更好的测试。避免这种情况的一个好办法就是编写行为驱动开发BDD式的测试。

Go with module 的项目结构实践

Go初学者经常遇到的一个常见问题是“如何组织我的代码?”,这个问题可以分解如下:

  1. 怎么样的项目结构方便用户导入我的代码?
  2. 编译打包用的命令怎么在代码中放置(译者注:或者说,用户如何运行)?
  3. 使用 Go with module 对原来的项目结构方式有什么影响?
  4. 多个 package 如何在一个 moudule 中共存?

网上能够找到的教程要么代码比较过时,要么过于复杂,因此我想写一个既简单又尽可能新一点的教程,并且提供一个简单的示例。 对于有经验的大佬可能也会带来一些帮助。

通过本文的项目结构可以实现:

  1. 一个 module 包含多个 package,每个 package 都可以单独的 import。同时也支持使用导入同一 module 的其他 package
  2. 只能被同一 modulepackage 导入的 Internal packages
  3. 用户可以使用 go get 安装的命令行或可执行程序。

本文中提到的用户,指的是使用这个模块的开发人员,方法是将模块 import 到他们的代码中,或者通过 go get

前言

本文展示的示例代码地址:https://github.com/eliben/modlib

在这个示例中,项目路径直接设置为和 module 同名,go.mod 文件包含了:

module github.com/eliben/modlib

在 Go 项目中,项目名和Github路径同名是很常见的操作。Go也支持自定义命名,但是这个不在本文的讨论范围。你也可以把 github.com/eliben/modlib 重命名为 github.com/your-handle/your-project 或者 your-project-domain.io,这个都是可以的。

但是模块名称非常重要,因为它就是用户代码中导入的名称:

Import path example with arrows showing module name and package

项目结构

示例项目的文件和目录结构如下:

├── LICENSE
├── README.md
├── config.go
├── go.mod
├── go.sum
├── clientlib
│   ├── lib.go
│   └── lib_test.go
├── cmd
│   ├── modlib-client
│   │   └── main.go
│   └── modlib-server
│       └── main.go
├── internal
│   └── auth
│       ├── auth.go
│       └── auth_test.go
└── serverlib
    └── lib.go

一些解释:

go.mod 是模块定义文件,它包含了所有依赖的其他模块。当然,目前这个示例项目没有任何依赖。模块依赖和本文要讨论的问题也没有太大关系,感兴趣的可以去看下官方的 blog 文章:第一篇第二篇第三篇

go.sum 是模块依赖项的校验和(译者注:用于比对本地依赖模块的版本,如果不一致会触发下载),由 go 工具管理,你不必关心它,但是这个文件应该和 go.mod 一起放在源代码控制中。

config.go 这是第一个需要我们关心的代码文件,包含了一个简单的功能1

package modlib

func Config() string {
  return "modlib config"
}

文件中最关键的是第一行 package modlib。由于这个文件在项目结构中是最顶层,所以将程序包名作为模块的名称。通过这种结构你可以在自己的代码中直接导入 github.com/eliben/modlib,代码如下(Playground Link):

package main

import "fmt"
import "github.com/eliben/modlib"

func main() {
  fmt.Println(modlib.Config())
}

显而易见的,如果你的模块要提供一个 package,或者你要从模块的顶层 package 中导出代码,那么你可以将所有代码放在这个模块的顶层目录中,并且将这个 package 作为模块路径的最后一个部分(除非你使用更灵活的虚导入2)。

其他 packages

接下来介绍下 clientlib 下的代码。

clientlib/lib.goclientlib package 的文件。这个文件的命名不是关键,起什么名字都可以,重要的是代码第一行声明的 package 名称必须声明为 clientlib。文件内容如下:

package clientlib

func Hello() string {
  return "clientlib hello"
}

在用户代码中,可以使用 github.com/eliben/modlib/clientlib 导入这个 packace,如下(Playground Link):

package main

import "fmt"
import "github.com/eliben/modlib"
import "github.com/eliben/modlib/clientlib"

func main() {
  fmt.Println(modlib.Config())
  fmt.Println(clientlib.Hello())
}

serverlib 目录包含了用户可以导入的另外一个 package。那里面没有什么新的知识点,只是展示了多个 package 如何并存在模块中。

关于 package 导入路径说明:根据用户的实际需求,可长可短。用户可见的 package 由模块根目录的相对路径确定,比如我们有个叫 clientlib/tokens 并且其中有代码的目录,如果用户需要的话,可以直接 import 路径为 github.com/eliben/modlib/clientlib/tokenspackage 以使用。

对于某些模块来说,有时直接 import 顶级目录就足够了。比如直接导入 modlib,就没有进行子目录的直接 import,但是所有的代码都在 modlibpackage 中,用户可以通过调用时多级调用的方式来使用。

命令行/可执行程序

有时候一些 Go 工程不是用于导出 package,而是直接用于发布可执行程序或者命令行脚本。如果你的项目不是用于这个目的,请忽略这个章节,不要在项目结构中添加 cmd 目录。

项目中提供所有命令行/可执行程序一般都放在 cmd 目录中,在本文示例中的命名为:

Path for commands in a repository

用户可以使用 go 命令行按照以下方式安装:

$ go get github.com/eliben/modlib/cmd/cmd-name

# Go downloads, builds and installs cmd-name into the default location.
# The bin/ directory in the default location is often in $PATH, so we can
# just invoke cmd-name now

$ cmd-name ...

在项目modlib中,提供了2个不同的命令行程序示例:mobile-clientmobile-server。在每一个示例中,代码都在package main中。每个命令行程序的入口文件都是main.go,但这不是必须的,只要是在代码首行指定package main的都可以用作入口文件。

示例的modlib是一个直接能够运行的项目,你可以在你的设备上安装并运行:

$ go get github.com/eliben/modlib/cmd/modlib-client
$ modlib-client
Running client
Config: modlib config
clientlib hello

$ go get github.com/eliben/modlib/cmd/modlib-server
$ modlib-server
Running server
Config: modlib config
Auth: thou art authorized
serverlib hello

# Clean up...
$ rm -f `which modlib-server` `which modlib-client`

现在我们关注一下 modlib-server 的代码

package main

import (
  "fmt"

  "github.com/eliben/modlib"
  "github.com/eliben/modlib/internal/auth"
  "github.com/eliben/modlib/serverlib"
)

func main() {
  fmt.Println("Running server")
  fmt.Println("Config:", modlib.Config())
  fmt.Println("Auth:", auth.GetAuth())
  fmt.Println(serverlib.Hello())
}

这段代码展示了如何从 modlib 模块中,导入其他的代码。请注意这里的用法,在Go语言中使用绝对路径导入是最好的方式。这种方式同时可以用于软件包和命令行/可执行程序。如果 clientlib 中的代码需要导入顶级目录的函数,可以使用导入 github.com/eliben/modlib 的方式。

内部的 Packages

另一个重要概念是在模块内部使用的 package,即不想让用户可以导出的内部(或者私有) package。这个概念非常重要对于符合语义化版本控制规范(SemVer)的 Go with module 项目,因为当你发布v1版本时,如果没有使用内部 packages所有的代码都将成为公开的API,无法满足语义化版本控制规范。因此,需要对用户只暴露需要能够实现功能的最小化API,模块自身的逻辑代码应当私有化。

package 路径中,Go编译器会把包含的 internal 关键字视为特殊路径,同一个 modulepackage 可以正常导入,但是用户(即 module 外的代码)无法导入,强行导入会收到以下错误信息:

use of internal package github.com/eliben/modlib/internal/auth not allowed

在示例的 modlib 项目中,只有一个 内部 packages,在实际的项目中,一般会有一堆内部 packages

在示例的 modlib 项目中,只有一个 内部 packages,在实际的项目中,一般会有一堆内部 packages

如果你想判断一个 package 是否应该设置为内部的,默认的回答应该是 YES。因为一个 内部 package 修改为公开只需要重命名+重新编译即可让用户导入,而一个公开 package 如果要修改为内部时则会非常痛苦(用户可能已经依赖了这个 package)。在符合语义化版本规范的文档 module 中(v1或者更高版本),由于破坏了向下兼容性这将体现为一个主版本号更新

我的建议将尽可能多的内容放入内部管理,不只是 Go packages,例如内部的站点源代码、用于项目内部的工具和脚本等。这个建议确保了用户看到的项目根目录是最小化、最清晰的。在某种程度上,这也是一种自主文档(self-documentation)的方式。用户在Github页面中可以马上看到并且了解他们所需要的内容。由于用户一般不会使用我用来开发模块的东西,因此将这些内容隐藏在内部将很有意义。

附注

  1. 需要注意的是 config.go 这个文件名是自定义的,并不是每个项目都应包含这个文件,这个只是 这个 项目结构的示例。请具体情况具体操作。另外,本文示例的只描述项目结构,所有的package 和文件名都是可以任意命名的。 ↩︎
  2. 译者注:vanity imports,这个名词居然没有比较正式的中文翻译。直接翻译的话是虚荣导入。如果有人有更好的翻译建议的话,可以留言哈~ ↩︎