Don't wanna be here? Send us removal request.
Text
페미니즘 속 남성
얼마 전에 아래와 같은 타래를 올렸다.
남자 페미니스트가 가장 해야하는 일은, 신성한 여성들의 대화엔 끼지 않고 조신하게 있다가 밖에선 남자 안티페미니스트를 패는 일이다. 다소 간략화되었고 비유적인 표현이지만, 이와 관련해서 글을 쓰고 있으며 4년 후에 공개됩니다. . .
— 제이미🍤 (@theeluwin) August 20, 2019
이 트윗에서 말하는 '글'이 이 글은 아니다. 다만 생각을 정리할겸, 추가적인 내용과 함께 글로 남겨둔다. 이 글은 아래의 타래와 위 타래를 재구성하여 쓴 글이다.
이 타래 말인데 https://t.co/nNodTnW8k1, 그래서 결국 내가 진짜로 하고 싶었던 말은 타래 맨 첫 트윗이 맞긴 하다. 뭔가 이름을 붙이고 싶은데 (이름이 이미 있을텐데 분명).
— 제이미🍤 (@theeluwin) August 23, 2019
요는, 남성은 여성들 사이에선 많이 들어야하며 남성들 사이에서 많이 말해야 한다는 것이다. 수많은 안티페미니스트 남성들(혹은, 때때로 스스로를 "이퀄리스트"라 주장하는 남성들)에게 너무나도 하고 싶은 말이지만, 페미니즘 속 남성들에게도 적용되는 '원칙'이다 (뉘앙스는 좀 다르지만). 이를 다소 격하게 표현한 것이 바로 저 "신성한 여성들의 대화에 감히 끼어들지 말고 조신하게 듣기만 하다가 바깥 세상의 안티페미니스트 남성을 패라"가 된다. 타래에서도 언급했듯, 이 "신성한~"은 영화 <300>의 패러디다.
페미니즘 담론을 크게 내부 담론, 외부 담론으로 구분하자면 (이러한 구분의 행위가 의미가 있는가는 둘째치고) 내부 담론은 여성에 대한 것, 외부는 남성에 대한 것이다. 여기서 여성과 남성은 광의의 의미로써 여성과 남성인데, 그나마 최대한 엄밀하게 말하자면 가부장제, 남성중심의 카르텔, 그리고 성별 패싱에 의한 차별과 결박에서 나타나는 계급의 타도자와 피타도자로 구분 할 수 있다. 그리고 다소 조심스럽지만, 성소수자 남성은 때때로 이 담론 내에서 타도자의 입장이 된다. 이는 다만 자격론을 말하고자 하는 것이 아니며, 흔히 '시헤남'이라 불리는, 시스젠더 헤테로 남성들의 "나도 살기 힘든데 내가 무슨 권력자라는 것이냐"는 주장이 침투하는 것을 경계하기 위함이다. 여담이지만 (본인 빼고 다 아는 사실이지만) 저 주장은 반지성주의에 가깝다.
이러한 담론 구분 하에, 남성은 내부 담론에서 설 자리가 없다. 정확히는, 설 자리가 없어야한다는것이 내 주장(원칙)이다. 그 이유는, 페미니즘은 여성들의 목소리로 기록되어야하기 때문이다. 그 중에서도 특히나 내부 담론은 더더욱 더 여성들의 목소리로 채워져야 한다. 페미니즘 내부 담론, 그러니까 페미니즘을 통해서 여성들이 어떠한 해방을 가질 수 있는지, 어떤 영향을 주는지, 그 과정 속에서 어떤 일이 일어나거나 일어나야하고, 문제점이나 한계점은 없는지 등은 이미 여성들에 의해서 활발하게 논의가 되고 있다. 페미니즘을 막 공부하기 시작하는 남성들이 종종 '통찰'했다고 착각하는 생각들은 거의 대부분 아주 오래전, 길게는 100년도 넘게, 짧게는 1970년대부터 다뤄졌던 논의들인 경우가 많다. 해방운동은 언제나 자주적이여야한다. 이 내부 담론에서 피타도자의 생각이 들어가는 것은 불필요하며, 때로는 간섭이 된다. 가령 현시대의 페미니즘 내부 담론 중 가장 대표적인 것이 바로 탈코르셋 운동인데, 탈코르셋을 하는 여성이 다른 탈코르셋을 하지 않는 여성을 비판하는 것은 정당한가? 이것은 어렵고 중요한 문제지만, 남성에 초점을 맞춘다면, 탈코르셋과 코르셋 그 어느것도 당사자가 아닌 남성이 할 수 있는 말은 없다. 그 논의의 몫은 오로지 여성들의 것이다 - 라고만 하면, 이는 다소 어려운 문제로부터 그저 회피하는것에 불과할 수 있기 때문에, 더 정확하게 말하자면, 그 논의의 과정이 기록될때 그 기록은 여성들의 목소리만으로 이뤄져야 한다는 것이다. 논의를 해야하는 책임이 있다는 것이 아니라, 논의를 할 권리가 있다는 것이다. 반대로, 남성들에겐 그 권리가 없다는 것이다.
비유와 추상화는 종종 각 사회운동의 고유 속성에 대한 본질을 흐리기 때문에 그 사용을 경계해야 할 필요가 있지만, 나는 "1968 Olympics Black Power salute"을 자주 인용한다. 아래의 그 유명한 사진이다.

이 사진에서 1위, 3위를 했던 토미 스미스, 존 칼로스는 신발 없이 서서 검은 스카프를 두르고 검은 장갑을 낀 채로 손을 들어 올렸다. 인종차별에 반대하는 블랙파워 시위를 한 것인데, 여기서 2위를 한 백인인 피터 노먼은 해당 시위의 앨라이임을 알리는 뱃지를 같이 달고 있다. 여기서 노먼은 (추방까진 안당했지만) 많은 백인들로부터 거센 비난을 받았다. 그렇지만 그는 검은 스카프도, 검은 장갑도 착용하고 있지 않았고 신발을 벗은것도 아니며 손을 들지도 않았다. 다만 뱃지를 착용하고 있었을 뿐이다. 왜냐하면, 신발과 스카프, 장갑과 손을 치켜드는 행위까지, 그 인종차별에 반대하는 시위의 주체는 흑인인 토미 스미스와 존 칼로스가 되어야하기 때문이다. 역사는 스미스와 칼로스를 운동가로, 노먼을 앨라이로 기록한다. 어디까지나 흑인 인종차별 반대를 위한, 흑인에 의한 흑인의 시위인것이다. 물론 노먼이 같이 검은 장갑을 끼고 손을 치켜들었다고 해서 의미가 퇴색되거나 하진 않았을 것이다. 다만 중요한것은, "흑인운동을 지지했던 백인이 있었다"가 아니라 "흑인들의 자주적인 운동과 시위에 의해서 인종차별이 (비록 진행형이지만) 철폐되었다"가 역사로 남아야한다는 것이다. 이 문장에서 백인에 대한 언급은 필요하지 않다. 나는 페미니즘 역사 속 남성은 노먼과 같아야한다고 생각한다. 그리고 어디까지나, 책임을 떠넘기는것이 아니라 여성들에게 권리가 있다는 것이다. 책임은 당연히 남성들에게 있다 (관련 영상, "잭슨 카츠(Jackson Katz): 여성 폭력은 결국 남성의 문제").
한편으로, 위 내부 담론 얘기는 시혜적 태도에 대한 경계와도 일맥상통 하는 면이 있다. 흑인운동에서 백인이 흑인에게, 사회주의운동에서 부르주아가 프롤레타리아에게, 독립운동에서 제국이 식민국에게, 어떠한 시혜적인 태도로 평등과 권리를 수여했다고 생각하면, 그건 사실 어색함 정도가 아니라 해방의 원칙에서 벗어남에 가깝다. 해방의 속성을 지닌 대부분의 운동에서 '자주'라는 키워드가 공통적으로 등장하는 것에는 그럴만한 이유가 있는 것이다. 피타도자의 시혜적 권력 양도로는 계급이 없어지지 않기 때문이다.
따라서, 이를 실천의 의미의 페미니즘에 적용했을때 남성이 페미니즘 내부 담론에서 설 자리는 없다. 페미니즘 속 남성의 행동은 항상 밖을 향해야 한다.
이 원칙 속에 있는 대표적인 책이 바로 <맨 박스>, <악어 프로젝트>, <저는 남자고, 페미니스트입니다> 등이다. 특히나 이 중에서 <악어 프로젝트>는 프랑스 여성들이 겪은 여러가지 일들을 모은 것이지만, 이 책은 페미니스트 남성이 다른 남성들에게 말을 하려는 책이다. 여담으로 나는 이 책을, 페미니즘을 공부하고자 하는 남성들에게 제일 먼저 권한다. 모든 남자를 악어로 표현해야하면 비로소 가해 남성에게 이입하는 것을 차단할 수 있기 때문이다.
얼마전 알게된 사회복지학적 인권 개념에 의하면, 시혜성은 사회복지의 구현에 있어서 현실적으로 필요로 하며 이 구현에 대한 논의가 생각 이상으로 중요하다는 것이다. 그래서 어쩌면 시���적 태도에 대한 철저한 경계와 궁극적으로 계급에 대한 완전한 타도를 해야한다는 나의 주장은 지나친 마르크스주의적 사고방식에 의한 것일 수도 있다는 생각을 했다. 이 부분은 계속 공부를 하는 중이다.
기회가 된다면, 이 글의 주 내용인 '페미니즘 속 남성이 하지 말아야할 것' 말고, '페미니즘 속 남성이 해야하는 것'에 대해서 글을 쓰고 싶다.
0 notes
Text
이 와중에도 과학에서 여성 지우기.
지난 4월 11일, 그러니까 ET(UTC-4) 기준 2019년 4월 10일 수요일 아침 9시에 인류 최초의 블랙홀 사진이 공개되었습니다.블랙홀이 어떻게 생겼는지, 많은 사람들이 시뮬레이션을 통해서 예측한 모습들이 있지만 실제로 '사진'을 찍은것은 이번이 처음인데요, 사실 이 '블랙홀 사진 찍기'는 지난 몇년간 진행 되어왔던 프로젝트였습니다.

2016년에, MIT 박사과정 학생이었던 케이티 부만(Katherine L. Bouman)은 한 논문을 발표하는데요 [1], 이 알고리즘은 블랙홀처럼 아주 멀리 있는 천체에 대한 관측 데이터를 처리하는 알고리즘에 관한 것이었고, 이를 사용해 블랙홀의 사진을 찍을 수 있다는 기사가 났습니다: "A method to image black holes". 이를 시작으로, 케이티 부만은 블랙홀 사진 촬영 프로젝트의 이미지 처리 시스템 개발 팀장을 맡게 되었고, 이어서 TEDxBeaconStreet 2016에서 "How to take a picture of a black hole"이라는 제목으로 강연을 하게 됩니다. 여담으로, 실제로 블랙홀 사진 촬영에 사용되었던 알고리즘은 TED 강연에서 다뤘던 내용과 정확히 같진 않고, [1]에서 시작된 케이티 부만의 알고리즘인 일명 CHIRP와 Jan Högbom이란 사람이 만든 CLEAN [2]이라는 알고리즘이 사용되었다고 합니다 [3].
이후 드디어 2019년 4월 10일 아침 (ET), 블랙홀의 첫 사진이 공개되었습니다 [3]. 소셜 미디어를 비롯한 많은 온라인 매체에선 케이티 부만이 첫 블랙홀 사진을 보는 모습, 수많은 하드디스크를 늘어놓은 모습 등 케이티 부만의 사진이 조명받게 됩니다: "블랙홀 사진을 만든 여성 케이티 부먼의 이야기", "The black hole image came thanks to student Katie Bouman, half a tonne of hard drives and a big coincidence".
Computer scientist Katie Bouman and her awesome stack of hard drives for #EHTblackhole image data 😍 — reminds me of Margaret Hamilton and her Apollo Guidance Computer source code. 👩🏽🔬 pic.twitter.com/MgOXiDCAKi
— Flora Graham (@floragraham) 2019년 4월 10일
이에 대해서 뉴욕 타임스에 "How Katie Bouman Accidentally Became the Face of the Black Hole Project"라는 제목의 기사가 나왔는데요, 요컨데 작은 프로젝트가 아닌만큼 수많은 사람들이 참여했을텐데 케이티 부만만 주목 받는것이 아니냐는 "논란"이 있었다는 것입니다 만, 실제로 기사 제목과 달리 내용은 "accidentally became the face"라기보단, 케이티 부만이 영광이며("It has been truly an honor"), 이 프로젝트는 한두 알고리즘 혹은 사람이 아닌, 전세계 수많은 과학자들과 함께 한 것("No one algorithm or person made this image. It required the amazing talent of a team of scientists from around the globe.")이다고 말한, 어쩌면 흔한 과학 기사의 내용입니다. 현대 과학은 많은 사람들이 팀을 이뤄서 나아가야 하는 경향이 큰 만큼, 당연한 코멘트라고도 볼 수 있습니다.
그런데 이게 레딧에서 얘기가 돌면서,
This garbage started on Reddit, where people looked through Bouman's GitHub to try to "prove" she didn't put in the work on the black hole image. It's all over "men's rights" subreddits like r/MGTOW. Here's her colleague explaining how they're wrong.https://t.co/akafhzvJwS
— Ben Collins (@oneunderscore__) 2019년 4월 12일
(아래 더 첨언하겠지만) 여성이 과연 이렇게 큰 이공계 프로젝트에 실질적 공헌을 했는지 의심을 하는 사람들이 생겨났습니다: "Online trolls are harassing a scientist who helped take the first picture of a black hole". 쉽게 말해 '검증'을 하기 시작했는데요, 그들이 찾아든건 케이티 부만의 깃헙이고, 여기서 이미지 처리 라이브러리인 eht-imaging을 찾게 됩니다. 그리고 이 repository가 대부분 Andrew Chael이란 사람에 의해서 작성되었기 때문에, 실제로 가장 큰 기여를 한 사람은 케이티 부만이 아니라 Andrew Chael이 되어야한다는 주장까지 나오게 됩니다. 이 연장선으로, "'여성'이 겨우 6%만 기여했지만 100%의 크레딧을 가져간다"는 유투브 영상을 비롯하여 페북, 인스타 등에서 여러 온라인 매체 찌라시가 생겨나고 (남성이 뭘 하면 '그 사람이', 여성이 뭘 하면 '여자가'가 되죠), 유투브에 "Katie Bouman"이라고 검색했을때 저 영상이 가장 상단에 뜨기도 했습니다.
YouTube algorithm vs. Katie Bouman pic.twitter.com/3TABQ2vitN
— Max Woolf (@minimaxir) 2019년 4월 12일
이러한 일련의 흐름에 대해서 Andrew Chael이 직접 말을 했습니다: [원문]
(1/7) So apparently some (I hope very few) people online are using the fact that I am the primary developer of the eht-imaging software library (https://t.co/n7djw1r9hY) to launch awful and sexist attacks on my colleague and friend Katie Bouman. Stop.
— Andrew Chael (@thisgreyspirit) 2019년 4월 12일
Adnrew Chael (@thisgreyspirit): "Our papers used three independent imaging software libraries (including one developed by my friend @sparse_k). While I wrote much of the code for one of these pipelines, Katie was a huge contributor to the software; it would have never worked without her contributions and the work of many others who wrote code, debugged, and figured out how to use the code on challenging EHT data. With a few others, Katie also developed the imaging framework that rigorously tested all three codes and shaped the entire paper as a result, this is probably the most vetted image in the history of radio interferometry. I'm thrilled Katie is getting recognition for her work and that she's inspiring people as an example of women's leadership in STEM. I'm also thrilled she's pointing out that this was a team effort including contributions from many junior scientists, including many women junior scientists. Together, we all make each other's work better; the number of commits doesn't tell the full story of who was indispensable. So while I appreciate the congratulations on a result that I worked hard on for years, if you are congratulating me because you have a sexist vendetta against Katie, please go away and reconsider your priorities in life."
그런데 놀랍게도 Andrew Chael 본인이 이렇게 말을 했는데도, 아래와 같은 멘션이 달려있습니다: [원문]
Of course she deserves credit but spinning it into a female success story is forcing a narrative on a project that many other faces including yourself deserve just as much if not more credit for. Thank you for clarifying facts but I still believe you deserve recognition
— Triston Lee (@Triston_Bowman) 2019년 4월 12일
Triston Lee (@Triston_Bowman): "Of course she deserves credit but spinning it into a female success story is forcing a narrative on a project that many other faces including yourself deserve just as much if not more credit for. Thank you for clarifying facts but I still believe you deserve recognition" 과학자가 여성이라는 이유로 과하게 띄워주는게 과연 옳으냐는 것인데, 제가 확실하게 말할 수 있는 것은 이 프로젝트에 대해서 "Woman Does 6% of the Work"라고 표현되고 그게 유투브 상단에 뜨는건, 확실하게 여성을 지우는 것이라는 겁니다. 띄워주긴 뭘 띄워줘 지우기 바쁜 사람들 주제에.
(추가 수정) * 관련 글 :
"Trolls hijacked a scientist’s image to attack Katie Bouman. They picked the wrong astrophysicist."
[관련 트위터 타래 1] (다른 동료인 Kazu Akiyama (@sparse_k)의 본 사태에 대한 글)
Hello there, I’m an imaging coordinator at the EHT (see https://t.co/Kj7wGeRPe2). I would like to provide some facts about our imaging group and my wonderful friend and colleague Katie Bouman for the sake of restoring her credit damaged by many inaccurate articles about her.
— Kazu Akiyama (@sparse_k) 2019년 4월 12일
[관련 트위터 타래 2] (사칭 계정 관련)
Not only are there a bunch of fake Katie Bouman @instagram accounts now, but they’re spreading the lie about her colleague writing most of the code and commenters are just eating it up anyway thinking they’re replying to the real deal. They even made a fake account for the guy. pic.twitter.com/Uqo2GhKOhc
— Gene Park (@GenePark) 2019년 4월 12일
이공계에 여성이 적은, 혹은 적어보이는 이유는 무엇일까요? 제 생각엔, 이공계에 여성이 적거나 적어보이기 때문입니다. 무슨말이냐면, 이공계 즉 STEM 분야에서 여성들은 기본적인 배려를 받지 못하거나 여성에게 적대적인 사람들 사이에 놓이고, 실존 권리를 온전히 보호받지 못하며 멸시 혹은 차별 대우를 받을 확률이 높다는 뜻입니다. 가장 기본적으로는, '정말로 이공계 분야에 필요한 능력이 있는지'를 끊임없이 '검증'하려 달려들죠 (We have nothing to prove to you). 이는 물론 이공계 분야만의 문제는 아닙니다. 여담으로 이 현상은 여성들을 향해서만 나타나는 현상이 아닌데, 하나만 예로 들자면 디자이너 출신 개발자가 있을때 그 사람에게 다른 개발자들이 가서 '정말로 개발에 필요한 능력이 있는지'를 '검증'하려 드는 것과 비슷합니다. 리눅스 명령어 같은걸 사용 할 줄 아느냐고 묻는다거나 하죠.
또 여담으로 얼마전에 비슷한 글을 페이스북에 올렸었는데, 이 글이 공유되고 거기에 달린 코멘트 중에서 어떤 남성분이 '나는 저런거 본적 없다'고 하신적이 있습니다 (지금 다시 보니까 공유가 취소되었는지 안뜨네요). 이는 사실 1. 당연한 말이지만 본인이 본적 없다고 없는 일이 되는건 아니며, 2. 남성이다보니 그 '검증'을 보통 당하는 입장이 아니라서 못봤을 확률이 매우 크며, 3. 주변 여성분들이 이런 일을 겪었다고 말하지 않는것이 어쩌면 바로 저렇게, '나는 저런거 본적 없다 (따라서 없는거다)'고 말할까봐 일수도 있습니다. 저는 <브로토피아 - 실리콘밸리에 만연한 성차별과 섹스 파티를 폭로하다>라는 책을 읽는것을 추천드립니다.
그럼 다시, 이공계에 여성이 적은, 혹은 적어보이는 이유는 무엇일까요? 요약하면, 가장 근간에 있는 "여성들은 과학 능력이 남성들보다 부족하다"라는 성차별적인 편견 그리고 역사에서 지워지는 여성 과학자들의 업적 때문입니다. 그리고 이 두가지는 직간접적으로 이공계 분야가 여성들에게 안전하지 않은 공간이 되게끔 만듭니다: "Seven ways the world is not designed for women".
여성들이 선천적인 과학 능력이 부족하다는 편견은 너무 낡았다고 생각하나요? 2004년만 해도 하버드대 총장이 직접 저런 말을 할 정도였습니다: "여성이 이공계 성공 드문 이유는 ‘과학꼴통’탓?". 그리고 저만 해도 저것과 비슷한 말을 하는 사람을 최근 1년간 3명 이상 만났습니다. 살면서 "여성들은 수학, 과학에 약하다"는 말을 하는 사람을 정말로 한명도 본적이 없나요? 그러고보니 그 사람들에게 저 편견에 대해서 추가적인 의견을 물으면, "맞잖아?"하는 대답이 돌아옵니다.. 아무튼, 이 편견은 생각 이상으로 많은 여성들을 옭아매고 있습니다. 다른 성차별과 마찬가지로, 한 남성이 수학을 못하면 '그 사람'이 수학을 못하는 것이지만, 한 여성이 수학을 못하면 '여자'가 수학을 못하는 것이 됩니다. 그래서 여성들은 충분한 이공계 능력이 있음을 매번 증명해야하는 상황에 놓여집니다. 컴퓨터공학과 여성 교수가 트위터에 자바와 관련된 농담을 올리면, 그 교수의 사진만 보고 남성들이 몰려와서 자바에 대해 설명을 시작합니다. 그 사람들이 사진만 보고 '여성들은 프로그래밍 언어에 대해서 잘 모를테니'라고 섣불리 생각하지 않고 프로필 페이지 한번만 체크했다면 "나는 컴퓨터공학과 교수고 나에게 자바에 대해서 설명하지 않아도 알고 있다"고 답변을 달지 않아도 되었을텐말이죠.
역사에서 지워지는 여성 과학자들[4]에 대해서 말하자면 사실 정말 끝도 없습니다. 아래는 관련 기사들인데 추천드립니다:
"Women have been written out of science history – time to put them back"
"How research erases women, the prehistory of Polynesia, and everything you wanted to know about beer: Books in brief" (이건 과학자에 대한게 아니라 '과학'이 여성을 지운다는 내용입니다)
"Why we need to stop erasing women from history"
"6 Women Scientists Who Were Snubbed Due to Sexism"
"The quest to reveal science’s hidden female faces"
제목부터 너무나도 멋진 의미를 담긴 영화, <히든 피겨스>도 위 주제에 대해 말하고 있죠. 역사 뿐만 아니라 일반 미디어에서도 쉽게 지워지는데요 (관련 기사: ‘여자=꽃, 남자=리더’ 아동 잡지에 새겨진 젠더 고정관념), 이는 다시 '이공계 여성은 적다'로, '여성들은 선천적으로 과학 능력이 부족하다'로, 또 '이공계는 여성에게 적합하지 않은 분야다'로 이어집니다. 이렇게 현재의, '여성들에게 안전하지 않은 이공계'라는 환경이 탄생하게 되었죠 (태초엔 그럼 왜 이공계에 여성이 적었을까요? 불과 백년 전만 해도 사회는 여성에게 과학의 '권위'와 '권한'을 주지 않았습니다).
사족으로, 저는 이공계 여성들에 대해서 '남자같다'라고 표현하는 것들이 모두 사라졌으면 좋겠습니다. '보통' 남자들이 많이 하는 이공계 분야, '보통' 남자들이 많이 하는 게임, '보통' 남자들이 좋아하는 영화나 미디어에 대해 여성들이 참여하게 되면, 위에서 말한 '검증'이 시작되거나 "넌 다른 여자들과 다르구나"와 같은 말을 하곤 합니다. 이에 대해 [5]에선 1. '여자'답지 않은 것에 대해 패널티를 받거나 2. '여자'지만 예외로 취급되거나 3. '여자답지 않다'라는 새로운 스테레오타입을 받게 된다고 합니다. 요약하면, '검증' 후 '인정' 과정이 이뤄진다는 것이죠. 어떻게든 사회에 악영향만 끼치는 결론이 납니다. 누가 누구를 왜 검증해서 인정하나요? "I have nothing to prove to you."
그러고보니 이와 관련된 내용들을 사람들과 말하면서, 이공계가 여성에게 '적대적이다'라고 하는것에 의문을 가진 사람들을 많이 만날 수 있었습니다. 요약하면, 본인은 적대적이지 않으며 주변 사람들도 적대적이지 않다는 것인데요, 이공계 여성들이 받는 이 적대감 즉 비존중에 대해서 말을 하자면, 컴퓨터비전 분야에서 역사 깊은 사진인 "레나" 얘기를 하고 싶습니다. 관련 기사는 다음 두개 입니다:
"How a photo from Playboy became part of scientific culture"
"Every Picture Tells A Story"
[관련 트위터 타래]
It's 2017, and researchers are still using Playboy's Lena centerfold as a test image. Given the gender issues in this field, maybe it's time to move on guys? 🤔 https://t.co/DJpuXCwR51
— Kate Crawford (@katecrawford) 2017년 11월 30일
논문에 레나 사진이 들어가게된 배경을 생각해보면, 한 남성 과학자가 플레이보이 모델 사진을 논문에 넣고 (유명해진 사진은 일부지만, 원본은 누드 사진입니다), 그걸 남성 과학자들끼리 공유하면서 그 사진의 정체를 모르는 다른 여성 과학자들이 자연스레 그 그룹에서 배척되는 상황을 상상해볼 수 있습니다. 너무 비약이 심한것 같나요? 저는 "이거 플레이보이 사진인거 알고 있어?"하고 여성에게 '떠'보는 남성을 지난 3년동안 두명 이상 봤습니다. 여성이 적고, 여성들이 불편함 혹은 불쾌감을 말하면 "왜 그리 예민해" 혹은 "뭐가 그리 불편하다고" 하는 이 상황 속에서, 당황스러울 정도로 당당한 성적 대상화 이미지를, 꼭 써야하는 이유가 없는데도 불구하고 컴퓨터비전 분야에선 하나의 전통처럼 자리잡았습니다. 여성과 여성 동료 과학자들에 대한 비존중의 역사를 그대로 가지고 가는 셈입니다.
더 직접적으로, 여성 과학자들이 겪는 적대감과 비존중은 과학의 주요 활동이라고 볼 수 있는 1. 글, 영상 등의 매스미디어와 2. 논문에서도 쉽게 찾아볼 수 있습니다. 유투브 영상 중 과학 관련된 영상 댓글을 분석해본 결과, 비판적인 내용의 댓글이 여성의 경우 14%, 남성의 경우는 6%였다고 합니다 [6]. 영상에 나오는 사람의 신체와 관련된 댓글은 4.5% 대 1.4%, 더 나아가서 성차별적이거나 성희롱에 해당하는 댓글은 3% 대 0.25%였다고 합니다. TED의 경우는 어떨까요? 과학 관련 TED 강연에서 발표자가 여성일 경우 약 15.28%의 댓글이 강연 내용과 관련이 없었다고 합니다 [7]. 남성의 경우 9.84%인데요. 논문과 연구, 그리고 교수 임용까지, 학계 전반에선 어떻게 나타날까요? 대표적으로 [8, 9, 10] 연구 결과에 따르면 박사 이후 포닥 혹은 그 이후 과정에서 여성들의 이공계 포기 비율이 남성들보다 높게 나타났으며 [9], 여기서 '포기'라는건 쉽게 말해 교수 임용 실패를 뜻하고, 교수 임용 과정에서 성차별 혹은 gender gap이 나타는건 (물론, 커리어를 포함한 변수들을 컨트롤 했을때) 기존 faculty가 남성 편향으로 이뤄졌을때만이었다고 합니다 [8]. 즉, 앞으로도 계속 편향적이 될 가능성이 크다는 뜻입니다. 요약하자면, 현존하는 아카데미는 여성들에게 적대적이고 성차별적이며 성희롱을 비롯한 성범죄가 만연하고, 이는 여성들이 학계에 남을 수 없게끔 압박을 주고 있다는 것입니다 [11]. 어쩌다 이렇게 되었을까요? 역사를 살짝 빌리자면, 아주 당당하게 이공계 분야에서 성차별을 하던 시절은 그닥 오래되지 않았고 그 문화의 offspring들이 아직도 현역이며, 실은 '아주 당당하게 여성을 배척하는' 환경은 역사속으로 사라진게 아니라 여전하기 때문입니다 [9]. 추가로, 이공계 분야 내에서도 '역사적으로' 여성들이 하던 일들 중 권력과 권위가 생길 수 있는 일임이 밝혀지면 어느새 '남성들의 일'로 탈바꿈 하죠. 이 모습을 그린게 바로 <히든 피겨스>고 그 주인공들 중 몇분은 아직 살아계십니다. 수학 계산'따위'는 '여자들이나 하는 일'이었는데 어느새 '여성들은 수학과 과학에 약하다'로 바뀐걸 보면 말이죠. 논문의 경우는 어떨까요? 흥미롭게도, 1저자의 성별에 따른 억셉률 차이는 미비하지만, 리뷰어가 모두 남성일 경우, 교신저자의 성별에 따라서 억셉률이 significant한 수준으로 차이가 나타났습니다 [12]. 이름이 '여성스러운지'에 ���라서도 차이가 나타났다는 얘길 들어본적이 있는데요 [레퍼런스 찾기 실패], 얼마전에 아시아인이 미국에서 좋은 대학에 가려면 '미국식' 이름을 써야 합격률이 올라간다는 얘길 들은게 생각나네요. 최근에 네이처에서 학계에서 여성들이 "leak"하게 되는 이유에 대한 논문이 발표되었습니다. 기사로 편하게 읽어보세요: "How the entire scientific community can confront gender bias in the workplace".
누군가는 (언젠가 누군가 저에게 그랬습니다) 여성들이 과학에 필요는 한가? 하고 물을수도 있습니다. 어디서부터 어디까지 대답해야할지 감이 잘 안잡히지만, 하나만 얘기하자면 현재 제가 몸담고 있는 딥러닝 분야에서는 딥러닝이 학습해버린 편견에 대해서 해결 하는 것이 이슈 중 하나입니다: WiNLP. '학습해버린 편견'은, 오답을 뜻하기 때문이죠. 관련 기사로, 조경현 교수님의 인터뷰가 있습니다: "자동번역이 똘똘해졌죠? 이 사람 덕분입니다".
* 관련 글:
"여성들이 과학 및 기술 분야에 종사하길 꺼리는 진짜 이유"
"왜 이공계는 ‘여성의 무덤’이 됐나"
References:
[1] Bouman, Katherine L., et al. "Computational imaging for vlbi image reconstruction." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2016.
[2] Högbom, J. A. "Aperture synthesis with a non-regular distribution of interferometer baselines." Astronomy and Astrophysics Supplement Series 15 (1974): 417.
[3] Akiyama, Kazunori, et al. "First M87 Event Horizon Telescope Results. I. The Shadow of the Supermassive Black Hole." The Astrophysical Journal Letters 875.1 (2019): L1.
[4] Ceci, Stephen J., and Wendy M. Williams. "Understanding current causes of women's underrepresentation in science." Proceedings of the National Academy of Sciences 108.8 (2011): 3157-3162.
[5] Betz, Diana E., and Denise Sekaquaptewa. "My fair physicist? Feminine math and science role models demotivate young girls." Social psychological and personality science 3.6 (2012): 738-746.
[6] Amarasekara, Inoka, and Will J. Grant. "Exploring the YouTube science communication gender gap: A sentiment analysis." Public Understanding of Science 28.1 (2019): 68-84.
[7] Tsou, Andrew, et al. "A community of curious souls: an analysis of commenting behavior on TED talks videos." PloS one 9.4 (2014): e93609.
[8] De Paola, Maria, and Vincenzo Scoppa. "Gender discrimination and evaluators’ gender: evidence from Italian academia." Economica 82.325 (2015): 162-188.
[9] Wenneras, Christine, and Agnes Wold. "Nepotism and sexism in peer-review." Women, sience and technology: A reader in feminist science studies (2001): 46-52.
[10] Bagilhole, Barbara. "How to keep a good woman down: An investigation of the role of institutional factors in the process of discrimination against women academics." British Journal of Sociology of Education 14.3 (1993): 261-274.
[11] Jagsi, Reshma, et al. "Sexual harassment and discrimination experiences of academic medical faculty." Jama 315.19 (2016): 2120-2121.
[12] Murray, Dakota, et al. "Gender and international diversity improves equity in peer review." BioRxiv (2018): 400515.
추가: 케이티 부먼의 사진이 이런식으로 추가적인 미소지니 소스로 사용되고 있습니다.
1 note
·
View note
Text
gitlab registry 삽질 기록
요약: 10번부터 보세요 (14번이 솔루션)
1. 지금 진행중인 프로젝트는 django를 백엔드로, vue를 프론트엔드로 쓰고 있다. 배포를 docker를 써서 하기로 했고 그래서 CI 세팅을 해야하는데, 몇가지 우여곡절이 있었다.
2. 우선 이미 동작하는 CI 파이프라인이 있었는데 (credit: @jinh574):
2.1 로컬에서 개발하면서 (그러니까 compose 파일을 잘 만들어놓고 docker-compose up -d --build 해가면서) repo에 push를 하고
2.2 그 repo는 공용 서버에 세팅해둔 gitlab에 있고,
2.3 push가 들어오면 gitlab이 (마찬가지로 공용 서버에 세팅해둔) jenkins에 webhook을 날리도록 설정해뒀고 (develop과 master 브랜치에 대해서만),
2.4 jenkins는 blue ocean 플러그인을 사용해서 repo 안에 있는 Jenkinsfile을 읽어서 스크립트를 실행하는데 (읽으려면 gitlab에 access token이 있어야해서 credential 설정을 해줘야한다),
2.5 스크립트 내용은 repo에서 소스코드를 pull 한 다음안에 있는 Dockerfile대로 docker image를 build하고 테스트를 돌리고 이를 공용 서버에 있는 registry에 push한 다음, develop이냐 master냐에 따라서 지정해둔 서버에 deploy를 하는것.
2.5.1 registry는 대충 세팅 해둔거라서 (public인 registry:2 image를 그냥 run 한 것) auth 설정이 없었고 그래서 외부 일반 접속 방지를 위해 htpasswd 설정 같은걸 했어야 했다.
2.5.2 deploy 스크립트 자체도 Jenkinsfile 안에 들어있긴 했는데, 각 서버 안에 미리 배치해둔 docker-compose.yml 파일에 있는대로 run 하는 것이다.
2.5.3 master 브랜치에서 접근하는 서버는 (그러니까 ssh를 통해야하기 때문에 관련 credential 세팅을 미리 jenkins 쪽에 해놔야한다) swarm으로 구성되어있어서 마스터 노드 쪽에만 up을 한다.
2.6 이 일련의 commit - ci trigger - source pull - build - test - release (image push) - deploy (server up) 과정은 blue ocean에서 이쁘게 그림으로 그려준다.
3. 근데 이왕 gitlab을 잘 쓰고 있으니까 더 잘 쓰기 위해서 gitlab ci를 활용해보기로 했는데,
3.1 처음엔 gitlab-runner라는 개념이 좀 헷갈렸다. 어쨌건 ci worker 역할을 하는거였고 gitlab 자체랑은 별도로 돌아가기 때문에 서버가 다른곳에 있어도 괜찮은 그런 친구였는데,
3.2 암튼 이 runner가 해줘야 할 일 중 하나는 docker image를 build하는것이라서 docker socket binding 방식으로 runner 세팅을 했고 (https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-socket-binding),
3.3 아주 간단한 파이프라인을 gitlab ci로 돌릴 수 있다는걸 확인 했다.
3.4. 이제 위의 2번 내용을 gitlab ci / runner 환경에서 해보려니까 registry가 하나 필요했는데, "이왕 이렇게 된거" gitlab에 새로 추가된 기능인 gitlab container registry를 사용해보기로 했다.
3.4.1 정확히는, 나만의 registry를 갖고 싶었는데 htpasswd로 접근 제한 하는거 말고, 계정 관리 같은거 하면서 UI도 이쁘게 되는 그런걸 하고 싶었고 portus가 그러한 솔루션이지만 (우여곡절 끝에 portus 세팅을 해냈다; 환경 변수 설정 할게 엄청 많다), 도메인이 문제였다.
3.4.2 기본적으로 gitlab도 그러하지만 registry는 https를 쓰는걸 강력히 권장한다. 문제는 학교 서버에서 작업하는거라서 도메인은 쉽게 얻을 수 있지만 80포트나 443 포트를 외부 접속이 가능하게 여는건 좀 복잡한 일이었다. 심사 비슷한걸 받아야함. 쓰고 있던 gitlab은 https가 있어서 그 서버에 세팅하려고 했던것.
3.4.3 처음엔 gitlab 서버에 나만의 registry를 세팅하려고 했는데 (그러니까 portus) 이미 gitlab registry 기능이 있는데 뭐하러? 그래서 gitlab registry 세팅을 시작했다.
4. gitlab을 설치한건 내가 아니기 때문에 gitlab 세팅법에 대해서 잘 몰랐다. 일단 확인해보니 웹서버는 apache2였고 gitlab 설치는 omnibus 설치 방식으로 되어있었다. 그래서 실제로 건드려야할것은 /etc/gitlab/gitlab.rb 파일.
5. 나의 경우는 설치된 gitlab과 동일한 도메인을 가진 registry를 세팅하려고 한거라서 https://docs.gitlab.com/ee/administration/container_registry.html#configure-container-registry-under-an-existing-gitlab-domain 에 나와있는대로 진행을 하려 했다.
6. 위 문서대로 /etc/gitlab/gitlab.rb 파일을 수정하고 `gitlab-ctl reconfigure; gitlab-ctl restart`를 했는데 뭔가 안되는것.
7. 확인해보니 gitlab에는 registry 탭이 잘 설치가 되어있고 registry 프로세스는 떠있는데, https://도메인:포트/v2/ 로 접속했을때는 안나왔다. connection error가 떴는데 그러니까 https와 지정 포트의 조합이 registry 프로세스가 먹고 있는 localhost:포트에 안닿는 것이었다.
8. 이는 /etc/gitlab/gitlab.rb에 세팅해둔 SSL 인증서들이 gitlab의 번들 nginx와 같이 쓸때만 먹히는것이고 지금은 apache를 쓰고 있으니까 실제로는 전혀 반영이 안되고 있었던것. 근데 내가 이때는 저 세팅 파일에서 지정하는 nginx 관련 변수가 "번들" nginx임을 몰라서, 단순히 apache를 내 개인(?) nginx로만 바꾸면 될거라고 생각했다.
9. 그래서 nginx로 세팅을 바꾸는 작업을 시작했는데 생각보다 원할하게 돌아가질 않았다.
9.1 우선 왠지 모르게 gitlab 관련 프로세스들이 (gitlab-ctl status 하면 보이는 친구들) 권한이 없다고 나오는것이다. 이걸 한참 못찾다가 gitlab-ctl tail로 쭉 로그 보면서 일부 socket들에 대해 nginx 프로세스가 권한이 없어서 bind를 못하고 있는거였는데 gitlab 설정 파일에 웹서버 유저를 설정해주고 www-data 계정을 gitlab-www, git 그룹에 추가해주는것으로 해결되었다. 알고보니 문서에 다 써있는 내용이었음 (https://docs.gitlab.com/omnibus/settings/nginx.html#using-a-non-bundled-web-server, 2번 맨 아래에 "*Note"로 적혀있다).
9.2 �� 9.1 과정에서 굉장히 삽질을 많이 했는데 nginx의 error log와 /var/log/gitlab/서비스들/current 파일들이 크게 도움이 되었다. 계속 에러 뜨는거 구글에 복붙만 하다가 정신 차리고 에러 로그를 잘 읽어보니까 해결이 되더라..
9.3 nginx로 바꾸는 과정에서 (웹서버 설정 파일은 gitlab이 템플릿을 제공한다: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server) SSL 인증서를 좀 만졌더니 이제는 같이 작업하던 동료들이 ssh로 repo pull/push가 안된다고 했다. 브라우저로 접속했을때는 https가 잘 먹혔는데 이건 좀 급하게 원상복구 해야 할 필요가 있어서 다시 apache로 돌아오기로 했다..
9.4 근데 그 와중에 뭔가 또 안되는게 있었는데, gitlab 세팅을 할 때 개인 apache를 쓰려면 unix socket 바인딩이 안되기 때문에 workhorse 서비스를 포트 바인딩 해줘야한다 (https://docs.gitlab.com/omnibus/settings/nginx.html#using-a-non-bundled-web-server 에서 4번). 시키는대로 /etc/gitlab/gitlab.rb 파일을 수정하고 다시 gitlab-ctl reconfigure를 돌렸다. 다시 apache로 잘 원상복구 됨. nginx로 바꾸면서 필요 없어보이는 설정을 죄다 지웠더니 이런 꼴이 났다.
10. 암튼 이제 다시 apache로 돌아왔는데 결국 7번의 문제로 돌아오게 되었고, 잘 생각해보니 gitlab 문서에는 개인 apache인 경우 registry를 설정하는 방법이 구체적으로 나와있지 않다는것을 알게되었다. 이는 이슈로도 올라와있다: https://gitlab.com/gitlab-org/gitlab-recipes/issues/50.
11. 즉, apache 설정으로 A포트(예컨대 4567)를 SSL로 묶고 외부에서 접속 가능하도록 만들고, 이를 내부 registry가 먹고 있는 B포트(예컨대 5000)로 proxy pass를 하는것이다.
12. 여기까지 하니까 이제 gitlab과 registry가 동시에 잘 작동했다. 그동안 gitlab을 자꾸 죽였다 살렸다 해서 야근하던 팀원들이 repo를 못쓰고 있었다.. registry가 gitlab에 묶여서 잘 작동한다는것은 https://도메인:A포트/v2/ 로 접속했을때 auth가 안되어있다는 메시지가 나오고, docker login 도메인:A포트를 했을때 gitlab 계정으로 인증이 가능하다는 것으로 확인 할 수 있다.
13. 문제는 이렇게 외부에선 A포트인데 내부에선 B포트일 경우, gitlab은 당연히 registry가 B포트에 있다고 생각하기 때문에 registry 탭에 들어갔을때 "B포트로 설정하고 로그인 하세요" 라고 알려준다. docker login 할 때도 포트를 적어줘야하고 (registry의 디폴트 포트는 5000번). 둘 다 5000번이면 충돌이 나서 안되기 때문에 분리해줘야한다.
14. 그래서 약간 흑마법을 쓰기로 했다. 좋은 방법이라고 할 순 없는데:
14.1 일단 웹서버를 내리고,
14.2 gitlab configure를 하는 시점에선 5000번이라고 알려준 후 (즉, /etc/gitlab/gitlab.rb엔 `registry_external_url "https://도메인:5000"`을 적는다),
14.3 이대로 configure를 한다 (gitlab-ctl reconfigure).
14.4 그다음 configure가 된, 실행중인 state라고 볼 수 있는 /var/opt/gitlab/registry/config.yml 파일에 addr를 localhost:4567로 바꾸고,
14.5 gitlab-ctl restart registry로 registry 서비스만 재실행 시켜준다. 그럼 gitlab은 문서를 만들때 5000번이라고 적지만 registry 프로세스는 4567번을 먹고 있다 (gitlab-ctl status로 registry가 run 상태인지 확인 + netstat -tulpn | grep 4567로 registry 프로세스가 4567번을 먹고 있는지 확인).
14.6 이제 웹서버를 다시 킨다. 웹서버 세팅에는 외부 https 5000을 내부 4567로 proxpy pass를 한다 (https://gitlab.com/gitlab-org/gitlab-recipes/issues/50 설정 파일 참고).
15. 그럼 이제 gitlab 문서에도 5000이라고 뜨고, 실제로 접속도 5000번, 로그인 할 때도 포트 없이 docker login 도메인만 하면 된다. 계정은 gitlab 계정을 쓰고.
16. 이 방법의 근본적 문제점은 gitlab을 껐다 키면 (서버 정전 등) 이 14번 과정을 다시 해줘야한다는 것인데 (/var/opt는 유동적이기 때문에) 이건 그냥 냅두기로 했다. gitlab은 내부 개발용 서비스고 서버관리자(나)가 항시 신경 쓰고 있기 때문에..
17. 사실 웹서버와 gitlab을 docker container로 띄웠다면 이런 문제가 없다.. 다 가짜 포트로 통신 하고 외부에 노출되는 포트만 그럴싸한걸로 고르면 되기 때문.
18. 결론: gitlab의 container registry 기능을 쓰려고 하는데 bundle이 아닌 웹서버, 그것도 apache를 쓰려면 좀 삽질을 해야한다.
0 notes
Text
python-crfsuite를 사용해서 한국어 자동 띄어쓰기를 학습해보자.
(이 글은 이 논문과 이 튜토리얼을 합친 것입니다)
python-crfsuite를 사용해서 한국어 자동 띄어쓰기를 학습해보자.
CRF는 레이블링 문제에 탁월하다고 알려져있으며, 여기서 레이블링 문제란 어떤 아이템의 시퀀스가 주어져 있을때, 각 아이템에 어떤 레이블(혹은 태그)가 어울릴지를 주변 아이템을 보고 유추하는 문제이다.
CRF에 대한 자세한 설명은 여기에서. 요약하자면 시퀀스에서 한 인덱스의 state와, 넘어가는 transition을 feature function으로 보고 계수를 multilinear하게 쭉 더해서 (softmax로) 조건부 확률을 계산하는 방식이다. 여기서 확률이 정답에 잘 맞도록 계수를 학습하는 딥러닝 알고리즘의 하나라고 볼 수 있다.
가장 간단한 예로, "마키 머리카락 먹고싶다"와 같은 문장이 있다면 각 음절을 아이템으로 보고, 글자 뒤에 띄어쓰기를 해야하는지 말아야하는지를 태그로 달아서 학습시킨 후에 띄어쓰기가 없는 문장을 주고 태그를 달아보라-고 할 수 있는 것이다.
"CRF를 이용한 한국어 자동 띄어쓰기"를 참고해서 태그를 달아보자면, 어떤 음절이 어절의 시작인지 아닌지로 각각 B, I로 태깅을 하게 되고 그 결과는 "마/B 키/I 머/B 리/I 카/I 락/I 먹/B 고/I 싶/I 다/I"와 같은 형식이 된다.
# -*- coding: utf-8 -*- from __future__ import unicode_literals, print_function import re import codecs def raw2corpus(raw_path, corpus_path): raw = codecs.open(raw_path, encoding='utf-8') raw_sentences = raw.read().split('\n') corpus = codecs.open(corpus_path, 'w', encoding='utf-8') sentences = [] for raw_sentence in raw_sentences: if not raw_sentence: continue text = re.sub(r'(\ )+', ' ', raw_sentence).strip() taggeds = [] for i in range(len(text)): if i == 0: taggeds.append('{}/B'.format(text[i])) elif text[i] != ' ': successor = text[i - 1] if successor == ' ': taggeds.append('{}/B'.format(text[i])) else: taggeds.append('{}/I'.format(text[i])) sentences.append(' '.join(taggeds)) corpus.write('\n'.join(sentences))
긴 글을 아무거나를 가져온 후 raw_train.txt에 저장 해놓고 위 코드를 사용해서 태깅된 학습용 데이터를 만들자. 대략 많으면 좋다.
raw2corpus('raw_train.txt', 'train.txt') # raw_train.txt에 뭔가 긴 글이 있음
예를 들어 raw_train.txt의 첫 줄이 "마키 머리카락 먹고싶다"라면 train.txt의 첫 줄은 "마/B 키/I 머/B 리/I 카/I 락/I 먹/B 고/I 싶/I 다/I"가 될 것이다.
참고로 태그된 데이터에서 복구를 하려면 위 코드와 반대로 짜면 된다.
def corpus2raw(corpus_path, raw_path): corpus = codecs.open(corpus_path, encoding='utf-8') corpus_sentences = corpus.read().split('\n') raw = codecs.open(raw_path, 'w', encoding='utf-8') sentences = [] for corpus_sentence in corpus_sentences: taggeds = corpus_sentence.split(' ') text = '' len_taggeds = len(taggeds) for tagged in taggeds: try: word, tag = tagged.split('/') if word and tag: if tag == 'B': text += ' ' + word else: text += word except: pass sentences.append(text.strip()) raw.write('\n'.join(sentences)) corpus2raw('train.txt', 'restored.txt') # restored.txt 첫 줄엔 "마키 머리카락 먹고싶다"가 있음
이제 태깅 작업은 끝났으니 파이썬 CRF 라이브러리 중 하나인 python-crfsuite를 설치하자.
$ virtualenv venv $ source venv/bin/activate (venv)$ pip install numpy scipy scikit-learn python-crfsuite
편의상 sent를 pair의 list로 두고
def corpus2sent(path): corpus = codecs.open(path, encoding='utf-8').read() raws = corpus.split('\n') sentences = [] for raw in raws: tokens = raw.split(' ') sentence = [] for token in tokens: try: word, tag = token.split('/') if word and tag: sentence.append([word, tag]) except: pass sentences.append(sentence) return sentences sent0 = corpus2sent('train.txt')[0] # [['마', 'B'], ['키', 'I'], ['머', 'B'], ['리', 'I'], ['카', 'I'], ['락', 'I'], ['먹', 'B'], ['고', 'I'], ['싶', 'I'], ['다', 'I']]
CRF에 학습시킬 feature를 정의해보자. 여기선 좌우 2글자까지를 보고 해당 인덱스의 글자에 태그를 맞추는 조건부 확률 문제로 생각하겠다. 즉, 위 예시에서 '머'가 왼쪽부터 '마키머리카'일때 (각 글자(이자 곧 feature)는 독립적으로 계산된다) B일 (조건부)확률, I일 (조건부)확률을 계산하는게 실제로 CRF가 하게 될 일이 된다. python-crfsuite에선 각 feature를 문자열의 리스트로 넘기면 된다.
def index2feature(sent, i, offset): word, tag = sent[i + offset] if offset < 0: sign = '' else: sign = '+' return '{}{}:word={}'.format(sign, offset, word) def word2features(sent, i): L = len(sent) word, tag = sent[i] features = ['bias'] features.append(index2feature(sent, i, 0)) if i > 1: features.append(index2feature(sent, i, -2)) if i > 0: features.append(index2feature(sent, i, -1)) else: features.append('bos') if i < L - 2: features.append(index2feature(sent, i, 2)) if i < L - 1: features.append(index2feature(sent, i, 1)) else: features.append('eos') return features def sent2words(sent): return [word for word, tag in sent] def sent2tags(sent): return [tag for word, tag in sent] def sent2features(sent): return [word2features(sent, i) for i in range(len(sent))]
(사실 이렇게 짜면 sent에 대해 루프를 계속 돌기 때문에 좀 비효율적이긴 하다. python-crfsuite의 튜토리얼과 최대한 비슷하게 하다보니..)
학습이 잘 되었는지를 알아보려면 테스트 데이터가 필요하다. 이건 학습된 CRF가 예상한것과 실제 정답을 비교해서 성능을 평가하는 것으로, 태깅이 되어있어야 하고 학습 데이터에 적당히 있으면서도 없���게 좋다. 가령 "츠바사 머리카락 먹고싶다" 같은걸 raw_test.txt에 적어놓고 태깅을 하자.
raw2corpus('raw_test.txt', 'test.txt')
이제 본격적으로 학습을 해보자. x는 feature를 추출한 sentence들이고, y는 정답이여야 하는 tag-sentence들이다.
import pycrfsuite train_sents = corpus2sent('train.txt') test_sents = corpus2sent('test.txt') train_x = [sent2features(sent) for sent in train_sents] train_y = [sent2tags(sent) for sent in train_sents] test_x = [sent2features(sent) for sent in test_sents] test_y = [sent2tags(sent) for sent in test_sents] trainer = pycrfsuite.Trainer() for x, y in zip(train_x, train_y): # 파이썬2에서 돌렸다 trainer.append(x, y) trainer.train('space.crfsuite')
뭔가 화면에 학습 iteration이 돌아가는게 좌르륵 뜰것이다. train함수에 verbose=False를 인자로 넘겨주면 안뜨게도 할 수 있다. 위 코드에서 space.crfsuite는 학습한 모델을 저장할 파일 경로가 된다.
이제 이걸 다시 불러와서 태거로써 사용해보자.
tagger = pycrfsuite.Tagger() tagger.open('locale.crfsuite')
이 tagger는 feature들을 받아서 태그 (B혹은 I)들을 반환 해주는 함수가 있다. 테스트 데이터에서 하나를 불러와서 태깅 해보자.
sent = test_x[0] print("Sentence: ", ' '.join(sent2words(sent))) # 츠 바 사 머 리 카 락 먹 고 싶 다 print("Correct: ", ' '.join(sent2tags(sent))) # B I I B I I I B I I I print("Predicted:", ' '.join(tagger.tag(sent2features(sent)))) # B I I I I I I I I B I
이걸로 이제 train.txt에서 학습한 모델로 띄어쓰기를 해볼 수 있게 된 것이다. 테스트 데이터에 대해 실제 결과물을 전체적으로 출력 해보고 싶다면 아래와 같이 하면 된다.
pred_y = [tagger.tag(x) for x in test_x] def flush(path, X, Y): result = codecs.open(path, 'w', encoding='utf-8') for x, y in zip(X, Y): result.write(' '.join(['{}/{}'.format(feature[1].split('=')[1], tag) for feature, tag in zip(x, y)])) result.write('\n') result.close() flush('pred.txt', test_x, pred_y) corpus2raw('pred.txt', 'raw_pred.txt')
이제 실제로 얼마나 정확한지 성능을 측정 해보자. 위에서 test_y는 실제 정답셋이고, pred_y가 이 신경망이 제출한 답안지인 셈이다. 이런 유형의 데이터를 분석하는데는 scikit-learn이 도움을 준다.
from itertools import chain from sklearn.metrics import classification_report from sklearn.preprocessing import LabelBinarizer def report(test_y, pred_y): lb = LabelBinarizer() test_y_combined = lb.fit_transform(list(chain.from_iterable(test_y))) pred_y_combined = lb.transform(list(chain.from_iterable(pred_y))) tagset = sorted(set(lb.classes_)) class_indices = {cls: idx for idx, cls in enumerate(tagset)} print(classification_report(test_y_combined, pred_y_combined, labels=[class_indices[cls] for cls in tagset], target_names=tagset)) report(test_y, pred_y)
위 코드대로 실행하면 대략 다음과 같은 결과를 볼 수 있을 것이다:
정말 작은 학습 셋으로도 생각보다 뛰어난 결과물을 준다. 꺄
3 notes
·
View notes
Link
(이 글은 사실상 이것의 번역이라고 볼 수 있습니다)
파이썬 gensim을 사용해서 한국어 Word2Vec을 구현해보자.
우선 Word2Vec은 간단하게 말해서 단어들을 고정된 차원의 벡터스페이스에 유의미하게 배치해주는 word embedding 알고리즘 중 하나이다.
예를 들면 king + woman - queen = man이나 한국 - 서울 + 도쿄 = 일본이 성립하도록.
이를 구현하는 방법에는 주변 단어들로 빈 자리에 들어갈 단어를 맞추는 CBOW과 주어진 단어로 주변에 들어갈 단어를 맞추는 skip-gram이 있다.
CBOW와 skip-gram에 대한 자세한 설명은 이쪽을 보면 되고, 이 글에선 Word2Vec 알고리즘을 내장하고 있는 gensim 라이브러리를 사용해서 빠르게 한국어 Word2Vec을 구현하는 것만 소개하도록 하겠다.
사실 이미 훌륭한 튜토리얼들이 많이 있긴 하다. 예를 들면 이런 글. 또, gensim의 Word2Vec 관련 함수들의 자세한 스펙이 궁금하다면 이쪽을 참고.
여기서 소개하는 구현법으로 그대로 하면 성능이 그닥 좋게 나오진 않는다.. 훌륭한 성능을 내고 있는 한국어 Word2Vec 사이트로는 elnn짱이 만든 Korean Word2Vec이 있으니 재밌는 결과가 보고싶다면 저쪽을 사용해보길!
gensim을 쓰기에 앞서 우선 corpus를 준비해야한다. 그리고 적당한 형태소 분석기를 사용해서 POS 태깅을 해주는게 좋다. 같은 단어라도 품사가 다를 경우 다르게 인식해줘야하기 때문이다. 특히나 "제이미가"에서 "가"는 조사인데, 이런걸 나눠줄 필요가 있기때문에 더더욱 해야한다. 영어였다면 단순한 tokenizing과 stemming으로도 충분 했을 것이다.
형태소 분석기별 차이는 여기에 자세하게 소개가 되어있으니 참고하면 좋다.
다만 Word2Vec의 주요 검색어는 명사가 될 것이기 때문에 (그 외 품사들은 학습용으로 사용된다고 생각하면 된다) 너무 세세하게 나눠주는 꼬꼬마나 한국어 Mecab보단 Twitter의 형태소 분석기가 더 적합하다고 판단했다.
from konlpy.tag import Kkma, Twitter Kkma().pos("오버워치") # [('오버', 'NNG'), ('워', 'UN'), ('하', 'XSV'), ('지', 'ECD')] Twitter().pos("오버워치") # [('오버', 'Noun'), ('워치', 'Noun')]
보통 단어와 그 단어의 POS 태그는 {:단어}/{:POS}와 같은 형식으로 표기를 많이 하기 때문에 이런식으로 나타내면 된다. 태그 결과는 DB에 저장해놔도 되지만 어차피 그냥 많은 텍스트만 대충 있어도 되기 때문에 파일에 써두자.
import codecs tagger = Twitter() corpus = codecs.open('corpus.txt', 'w', encoding='utf-8') def flat(content): return ["{}/{}".format(word, tag) for word, tag in tagger.pos(content)] corpus.write(' '.join(flat("메이드복 입은 제이미 귀엽다")) + '\n') # 메이드/Noun 복/Noun 입은/Verb 제이/Noun 미/Adjective 귀엽/Adjective 다/Eomi
코퍼스에서 적당히 영어나 숫자, 특수문자 등은 지우거나 말거나를 한 후에 문장별로 나눠서 한줄에 한 문장이 들어가도록 저장해두자. 역시나 DB에 넣어놔도 된다. 느린 몽고DB라던가.
gensim의 Word2Vec 함수는 다음과 같은 형식으로 문장을 인식한다.
import gensim sentences = [["my", "name", "is", "jamie"], ["jamie", "is", "cute"]] model = gensim.models.Word2Vec(sentences)
더 정확하게는, iterable한 단어들의 iterable이기만 하면 된다. DB에서 한줄씩 읽어도 되고, 파일을 열어서 한줄씩 처리해도 된다.
한줄이 곧 한 문장임을 가정하고, 문장 내에서는 띄어쓰기로 단어가 구분되어있다고 가정하면 다음과 같이 reader를 짤 수 있다. 파일이라면 그냥 열어서 split 하기에는 용량이 너무 크다.
class SentenceReader: def __init__(self, filepath): self.filepath = filepath def __iter__(self): for line in codecs.open(self.filepath, encoding='utf-8'): yield line.split(' ')
gensim의 Word2Vec 함수는 사전을 만드는 단계와 실제로 학습을 하는 단계로 나눠져있다. 위와 같이 iterator을 짜버리면 iteration이 끝난 후에 다시 읽으려고 할때 아무것도 읽을 수 없기 때문에 나눠서 넘겨줘야한다. 오히려 그렇기 때문에 사실은 사전 생성용 코퍼스와 학습용 코퍼스의 분리도 가능하다.
sentences_vocab = SentenceReader('corpus.txt') sentences_train = SentenceReader('corpus.txt') model = gensim.models.Word2Vec() model.build_vocab(sentences_vocab) model.train(sentences_train)
실제로 저걸 돌려보면 사실상 끝이다. 이제 코퍼스의 상태와 Word2Vec 파라미터에 따라서 결과만 달라질 것이다. 그리고 gensim은 꽤나 빠르기 때문에 POS 태깅 후 약 6기가 정도 나오는 코퍼스 학습에 30분 정도밖에 안걸린다. 다른 Word2Vec 구현체인 deeplearning4j와 비교해보면 상당히 빠른 속도이다.
추가로, gensim과 dl4j의 Word2Vec 결과물이 어느정도 다른지 간단한 실험을 해본 결과는 다음과 같다:
코퍼스는 dl4j의 Word2Vec 예제에 들어있는 raw_sentences.txt라는 파일을 사용했고, 동일한 파라미터로 돌렸으며 주요 단어짝들의 similarity 값이 얼마나 다르게 나왔다를 비교해본 결과이다. 내부가 구체적으로 어떻게 다르길래 이정도 차이가 나나 싶긴 하지만.. 목적에 따라선 dl4j를 쓸 이유가 없을수도 있다. dl4j는 GPU를 사용해도, 메모리를 엄청나게 많이줘도 느리긴 느리다.
어쨌거나 다시 gensim쪽 이야기로 넘어와서, 학습한 모델은 다음과 같이 저장 혹은 로드 할 수 있다.
model.save('model') model = gensim.models.Word2Vec.load('model')
이상한 파일들도 같이 생겼을것이다. 여튼 이제 이 model이라고 써둔 Word2Vec 객체만 있으면 여러가지를 할 수 있다. 가령 맨 위에 소개해둔 한국 - 서울 + 도쿄를 계산하려면
model.most_similar(positive=["한국/Noun", "도쿄/Noun"], negative=["서울/Noun"], topn=1) # [("일본/Noun", 0.6401702165603638)]
처럼 사용하면 된다. 이 함수는 cosine similarity로 비슷한 벡터를 찾아준다.
학습용 파라미터로는 어떤게 적합할까? 사실 그때그때 다르지만 나의 경우는 다음과 같이 세팅했다:
import multiprocessing config = { 'min_count': 5, # 등장 횟수가 5 이하인 단어는 무시 'size': 300, # 300차원짜리 벡터스페이스에 embedding 'sg': 1, # 0이면 CBOW, 1이면 skip-gram을 사용한다 'batch_words': 10000, # 사전을 구축할때 한번에 읽을 단어 수 'iter': 10, # 보통 딥러닝에서 말하는 epoch과 비슷한, 반복 횟수 'workers': multiprocessing.cpu_count(), } model = gensim.models.Word2Vec(**config)
여튼 이렇게 해서 한국어 Word2Vec을 간단하게 구현해볼 수 있다. 정확히는 구현이라기보단 그냥 라이브러리를 사용한것 뿐이지만..
나무위키, 한국어 위키피디아, 그리고 곰TV 자막을 코퍼스로 사용해서 위 코드를 거의 그대로 적용한 결과를 word2vec.theeluwin.kr에 올려놨다. 코퍼스의 전처리와 파라미터 튜닝 등의 부족 탓인지 성능이 그닥 좋진 않다..
0 notes
Link
파이썬으로 3줄 요약기를 만들어보자.
자동으로 요약해주는 알고리즘에는 다양한 방법이 있지만 여기서는 본문의 문장을 그대로 활용하는 Extraction-based summarization을 사용할 것이다.
단순하게는 각 문장별로 중요도를 annotate한 후에 높은 순서대로 출력하면 방법이 있을테고, 그 '중요도'를 어떻게 측정하냐가 문제다. 이는 가장 유명하면서도 단순한 TextRank 알고리즘으로 시험 해볼 수 있다.
TextRank 알고리즘은 구글의 PageRank 알고리즘을 기반으로 되어있다. PageRank에 대해선 이 글에서 재밌게 소개가 되어있으니 읽어보면 좋다.
PageRank 알고리즘의 기본 원리는 그래프로 데이터를 표현한 후, 각 edge의 값이 영향력을 행사한다고 보고 가장 중요한 node를 찾는 것이다. 비슷하게 어떤 글의 문장들도 서로 영향력을 행사한다고 생각하고, 여기서 가장 중요한 문장을 찾으면 된다.
우선 주어진 글을 문장별로 쪼개보자.
import re def xplit(*delimiters): return lambda value: re.split('|'.join([re.escape(delimiter) for delimiter in delimiters]), value) xplit('. ', '? ', '! ', '\n', '.\n')("This is a sentence. Here is another sentence.\nHello, world!") # ['This is a sentence', 'Here is another sentence', 'Hello, world!']
이렇게 rule-based로 문장을 구분하는건 그닥 좋은 방법이 아니지만, 널리 알려진 한국어 형태소 분석기들 중에선 빠르고 적절하며 원문을 보존하며 문장 구분을 해주는 기능이 구현된게 없고 이렇게 해도 일단은 큰 무리 없이 문장을 잘라준다.
첨언하자면 KoNLPy에 들어있는 형태소 분석기들 중 꼬꼬마는 문장을 잘라주는 기능이 있지만 이는 POS 태깅을 전부 한 후에 학습된 문장 구분자로 잘라서 POS가 띄어쓰기로 나눠진채 반환하기 때문에 원문이 유지되지 않는다.
from konlpy.tag import Kkma kkma = Kkma() kkma.sentence("이세돌은 알파고를 이겼다. 이세돌은 강하다. 알파고도 짱쎔.") # ['이세 돌은 알 파고를 이겼다.', '이세 돌은 강하다.', '알 파고도 짱 쎔.']
다음으로는 이제 '문장간 영향력을 행사한다'를 정의할 차례다. 여러가지 방법이 있겠지만 가장 쉽게 '두 문장의 공통으로 등장하는 명사에 의한 유사도'로 해보자. Similarity라는 것은 symmetric하기 때문에, 두 문장은 서로에게 동일한 값 만큼 영향력을 주게된다.
우선 명사는 다음과 같이 얻어낼 수 있다.
kkma.nouns("눈보라에 차갑게 얼린 머리카락 먹고싶다.") # ['눈보라', '머리카락']
실제로는 POS 태깅을 한 후에 결과가 명사라고 볼 수 있는 것들을 필터링해서 반환해주는 함수다.
같은 명사가 한 문장에 여러개 나올 수도 있다. 꼬꼬마는 중복을 제거하니까 트위터 형태소 분석기를 사용해보자.
from konlpy.tag import Twitter twitter = Twitter() twitter.nouns("미쿠 미쿠 하게 해줄게") # ['미쿠', '미쿠']
이제 얻어낸 명사들을 Bag-of-words, 혹은 멀티셋 즉 중복을 허용하는 집합으로 표현해놓자. 유사도는 단순히 두 문장에서 어떤 명사가 공통으로 (얼마나) 들어있나 안들어있나로 계산 할 것이기에 순서를 고려하지 않기로 한다. 그리고 파이썬엔 마침 Counter라는게 존재한다.
from collections import Counter bow1 = Counter(twitter.nouns("미쿠 미쿠 하게 해줄게")) # Counter({'미쿠': 2}) bow2 = Counter(twitter.nouns("미쿠 머리카락 맛있겠다")) # Counter({'미쿠': 1, '머리카락': 1})
두 멀티셋의 유사도는 Jaccard Index를 사용할 것이다. 식은 아래와 같다.
\(J(A, B) = \cfrac{|A \cap B|}{|A \cup B |}\)
멀티셋에서의 합집합은 key가 어느 집합에 있고, 그 수는 최대값. 그리고 교집합은 반대로 key가 두 집합 모두 있고, 그 수는 최소값이 되도록 정의된다. 예를 들면 다음과 같다:
\(\{1, 1, 1, 3\} \cap \{1, 1, 2\} = \{1, 1\}\)
\(\{1, 1\} \cup\{1, 2\} = \{1, 1, 2\}\)
그리고 매우 편리하게도 Counter가 이런걸 다 알아서 해준다!
j_index = sum((bow1 & bow2).values()) / sum((bow1 | bow2).values()) # 0.3333333333333333
이제 '문장간 영향력을 행사하는 정도'가 정의 되었다. 두 문장에서 공통으로 등장하는 명사들을 멀티셋으로 만든 후 나오는 Jaccard Index의 값을 '서로에게 이만큼 영향을 준다'로 정의한 것이다.
사실 이 말고도 여러가지 방법이 있다. 명사 말고 동사도 포함시켜도 되고, 레벤슈타인 거리를 사용해도 되고, TF-IDF를 계산해서 문장간의 각도를 사용해도 된다.
여튼 이제 문장을 node로 뒀을때 edge에 부여할 값을 계산 할수 있게 되었으니 남은건 이걸로 그래프를 그리고 거기서 PageRank를 돌리는 것이다.
그리고 이건 파이썬 라이브러리인 NetworkX가 알아서 다 해준다!
실제로 구현을 해보자면 일단 Sentence 클래스를 만들고
class Sentence: @staticmethod def co_occurence(sentence1, sentence2): p = sum((sentence1.bow & sentence2.bow).values()) q = sum((sentence1.bow | sentence2.bow).values()) return p / q if q else 0 def __init__(self, text, index=0): self.index = index self.text = text self.nouns = twitter.nouns(self.text) self.bow = Counter(self.nouns) def __eq__(self, another): return hasattr(another, 'index') and self.index == another.index def __hash__(self): return self.index
(여기서 index는 원문에서의 문장 위치를 나타내는데 쓰인다) 주어진 글을 이 Sentence 인스턴스의 리스트로 반환해주자.
def get_sentences(text): candidates = xplit('. ', '? ', '! ', '\n', '.\n')(text.strip()) sentences = [] index = 0 for candidate in candidates: candidate = candidate.strip() if len(candidate): sentences.append(Sentence(candidate, index)) index += 1 return sentences
이를 활용하여 그래프를 만들�� 다음과 같이 된다. 각 edge의 weight가 두 문장 간의 Jaccard Index가 되도록.
def build_graph(sentences): graph = networkx.Graph() graph.add_nodes_from(sentences) pairs = list(itertools.combinations(sentences, 2)) for eins, zwei in pairs: graph.add_edge(eins, zwei, weight=Sentence.co_occurence(eins, zwei)) return graph
이제 NetworkX의 pagerank 함수까지 사용해서 전체적으로 쭉 돌려보면
sentences = get_sentences(text) graph = build_graph(sentences) pagerank = networkx.pagerank(graph, weight='weight') reordered = sorted(pagerank, key=pagerank.get, reverse=True)
가 된다. 이제 reordered[0]엔 이 알고리즘이 생각하기에 가장 중요한 문장이 들어있을 것이다.
사실 이 방법은 문서 요약에 있어서 그닥 유효한 전략은 아니다. 문장간 영향을 주는것도 symmetric하고 결과적으로 '가장 중요한 단어가 가장 많이 들어간 문장'이 나오는데 이게 꼭 좋은 요약이 되지는 않으며 모든 문서에서 항상 통하는건 아니기 때문이다.
간단하게는 여기서 테스트를 해볼 수 있다. 자세한 소스코드는 (이 글에서 소개한 것과 100% 일치하지는 않지만) 깃헙에 올려뒀고, PyPI에도 올라가 있으므로 간단하게
pip install textrankr
로 설치해서 사용해볼 수 있다. 참고로 이 코드는 미흡한 점이 매우 많다.. PR 호시이
여튼, 3줄 요약기는 간단하게 이런 방법으로 구현해볼 수 있다.
→ PR 하러 가기
→ 팔로우 하러 가기
7 notes
·
View notes
Text
웹에서 간단하게 LaTeX을 쓰고 싶을때 cheatsheet
원하는것:
대충
Let $x \in \mathbb{R}$ be $x^2 = 0$. Prove $x = 0$.
이렇게 쓰면
Let \(x \in \mathbb{R}\) be \(x^2 = 0\). Prove \(x = 0\).
이렇게 보여지는 기능이 필요할때
<script type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}});</script> <script src="//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
를 적절한 위치에 삽입
끗
0 notes
Text
linux에서 ipa 다루기
흔히 Mac Command Line Tools로 ipa를 주로 다루게 되는데, 사정상 linux에서 다뤄야 할 때 필요한 몇가지 팁이다.
우선 패키지를 까는거. TestApp.ipa가 있다고 하면 zip이라고 가정하고 까면 된다. 그럼 Payload/TestApp.app이 나오는데 걍 들어가면 된다.
$ mv TestApp.ipa TestApp.zip $ unzip TestApp.zip $ cd Payload/TestApp.app
일단 가장 중요한 Info.plist를 파싱해보자. 보나마나 파일이 깨져있을 것이다. plutil에 한번 통과 시키고
$ plutil -i Info.plist -o Info.plist
그 파일을 파이썬 기본 라이브러리 중 하나인 plistlib으로 파싱한다.
>> import plistlib >> plist = plistlib.readPlist('Info.plist')
그럼 이제 plist가 dict 형태로 나온다.
>> plist['CFBundleExecutable'] >> 'TestApp'
다음은 아이콘 추출. Info.plist에는 아이콘 파일의 확장자가 없을 수도 있다. Icon.png 대신에 Icon이 있는 빡치는 상황. 알아서 잘 처리하면 된다.
>> import os >> icon_name = plist['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles'][-1] >> pipe = os.popen('ls | grep ' + icon_name) >> real_name = pipe.readline().strip() >> icon_format = real_name.split('.')[-1] >> file_name = '.'.join((icon_name, icon_format))
근데 또 이 png파일이 깨져있다ㅋㅋ pngdefry라는 툴을 이용하면 된다. 맥에선 커맨드라인툴이 제공된다. 저 파일을 받으면 맥용 바이너리가 포함되어있기 때문에 리눅스에서 쓰려면 빌드를 해야한다.
$ wget http://www.jongware.com/binaries/pngdefry.zip $ unzip pngdefry.zip $ cd pngdefry/source $ gcc -o pngdefry pngdefry.c $ sudo mv pngdefry /usr/local/bin
사용법은 홈페이지 참고 혹은
$ pngdefry -s _pure Icon.png
이러면 Icon_pure.png라는 안깨진 png가 나온다.
다음은 등록된 기기 정보가 들어있는 embedded.mobileprovision차례. 맥에선 걍 secure 써서 하면 된다.
$ openssl smime -inform der -verify -noverify -in embedded.mobileprovision > embedded.plist
그럼 이제 위의 Info.plist처럼 embedded.plist를 파이썬으로 파싱해서 사용하면 된다.
(계속 추가 할 예정)
0 notes
Link
github에서 맨 마지막에 newline을 안쓰고 diff를 보면 "AWWWWWW no newline at the end! kill me!!!"라고 외치는것만같은 circle-cancel 아이콘이 뜬다.
원래 코드에서 맨 마지막줄에 newline을 추가하는건 C-계열 때문이라고 알고있긴 하는데, 여튼 안하면 찝찝해서 html이든 js든 그냥 다 해줌.
이거랑 PEP8에서도 있는건데, 걍 탭이나 스페이스만 잔뜩 있고 엔터쳐서 넘긴 행들을 git diff로 보면 (터미널 색깔 설정에따라 다르지만) 엄청나게 불쾌감을 주게 표시해준다. "AWWWWW I only have tabs and blanks! Why do I live? kill me!!!" 같은 느낌?
여튼 이 두개를 처리하는 정규식을 포함해둔 플러그인. 기본적으로 4 space - 1 tab 변환 같은건 대부분의 IDE에서 제공해주는데, 그렇지 않은 경우들을 앞으로 계속 추가 할 예정.
Coda 2 용 플러그인 만드는건 stdin으로 소스코드를 받고 stdout으로 원하는걸 출력하기만 하면 된다. 엄청 쉬움.
0 notes
Text
새로운것이 익숙할때 혁신이 된다.
나는 UI를 "편리함", UX를 "익숙함"으로 정의를 하는데, 이는 사실 꽤나 일반적으로도 중요한 기준이 된다. 이를테면 세벌식 키보드의 "편리함"은 두벌식 키보드의 "익숙함"을 이길만큼은 되지 못해서 쓰이지 않는다고 볼 수 있다.
예를 한개 더 들면, 회원가입을 할 때 버튼을 눌러서 submit 하는건 submit 하기에 편리한 UI를 제공한것이고, 그 버튼이 버튼처럼 생겨서 "이걸 눌러야 합니다" 라고 알려주지 않아도 유저가 알 수 있게 하는게 좋은 UX다.
여기서 "혁신"이라는 수식어가 붙을 만한게 있다면, 편리하고 좋은 UI를 새로 만들었는데 그게 유저들에게 익숙하고 당연하게 받아들일 수 있을 때 이다. 즉, 교육에 들이는 시간이 0에 가까울때.
여러가지 예제를 들 수 있겠지만 내가 말하고 싶은건 두 가지 케이스. 첫째는 아이폰 알림센터에서 알림을 삭제 할 때 인데, 상단 우측에 있는 "x" 버튼을 누르면 "지우기"로 바뀌고, 그걸 한번 더 눌러야 삭제가 된다.
보통 아이템을 삭제하는 행위는 "정말로 삭제하시겠습니까?" 라는 물음을 던져서 실수를 방지해줘야하는데, 그걸 팝업창으로 처리하려면 너무 번거롭다. 그나마 웹에선 모달로 처리하려는 시도가 많았는데, 어찌됬든 눈이 즐겁지 못하다. 이 "x"에서 "지우기"로 바뀌는 UI는 분명 새로 등장한 것인데, 편리하고 익숙하며 별도의 교육과정없이 모두가 당연하게 사용하고있다. "한번 더 물어보는 것을 팝업없이" 라는 문제를 깔끔하게 해결한것이다.
두번째 케이스는 "당겼다가 놓아서 업데이트". 등장한지 꽤 된 것이긴 하지만, 나는 이런것이야말로 혁신다운 혁신이 아닐까 싶다. 계속 당기는 행위는 유저가 "새로운 것을 보고싶다" 라는걸 무의식중에 표현하는 것이고, 그에 따른 보상을 해줘야 하는데 실수로, 혹은 재미삼아 당긴것과 실제로 업데이트를 원하는 것을 구분지어야한다. 당겼다가 "놓음"으로써 "업데이트를 하라" 라는 "명령을 내렸다"가 인식되는 것이다. 그러면 빙글빙글 돌아가는 톱니를 보면서 기다리는 것이 전혀 부담스럽지도, 당황스럽지도 않다. 당연하게 받아들일 수 있다. 다만 여기서 한가지 문제가 발생한다. "당겨라"와 "당겼다가 놔라" 라는것을 확실히 구분해야한다는 것이다. 분명 당겼다가 놓는 것은 업데이트하기에 편리한 UI를 제공하는데, 이를 "업데이트 하려면 당겼다가 놓으세요" 라는 문구를 다 읽지 않아도 유저가 알 수 있게끔 해야한다. 새로운 UI가 익숙해야하는 모순을 해결했어야했다. 그리고 이걸 화살표로 풀었다.
분명 당기는 이벤트에서, 몇 px 이상 당기면 업데이트가 트리거되도록 설정했을것이다. 근데 그걸 유저에게 어떻게 전달 하느냐? 화살표를 돌린것이다. 아래로 향하던 화살가 빙글 하고 돌아가서 위로 향하게 된다. 구체적인 언급은 없었지만, "이제 그만 놔줘도 된단다" 라는 메시지를 남긴것이다. 애초에 당긴 이유가 "업데이트를 원한다" 였고, 그래서 당겨본건데 당기는 중에 화살표가 돌아갔다. 그것도 위쪽 방향으로. 이러한 작은 변화가 유저에겐 "놓으라" 라고 말하고 있지만, 유저가 업데이트를 원하고 있는 상황에서 생긴 변화였기 때문에 "당겼다가 놓다" = "업데이트를 한다" 라고 확실하게 인식이 된것이다. 화살표 하나로.
혁신이란건 이런게 아닐까 싶다. 스마트폰이란게 뭔지 사람들에게 하나하나 교육시켜주지 않아도 알아서 잘 쓴다. 물론 저런 풀이법을 찾는 것이나 스마트폰을 개발하는건 힘든 일이고 NP 문제라서 솔루션을 찾는건 정말 어렵다. 다만 우리가 당연하게 받아들이는 모든게 사실상 혁신의 결과물이다.
사람들이 필요한건 날아다니는 자동차가 아니라 바퀴달린 의자다.
1 note
·
View note
Text
초보자용 django deployment
목표 서버 설치 후 "Hello, world!" 출력하기
django의 deploy 환경은 모든 레이어에 대해서 "취향껏"이다. 때문에 순수 웹 프론트엔드 출신 개발자가 바로 익히기엔 초기장벽이 좀 있다.
알고나면 "취향껏" 쓸 수 있지만, 맨 처음 배울땐 full-reference가 없어서 약간의 삽질이 요구된다. 나야말로 "순수" 웹 프론트엔드 출신이었기 때문에 거의 한달 가까이 삽질만 했다.
아래 설명은 내가 사용하는 uwsgi 기반 deploy 환경. 아직 나는 클라우드 경험이 많지 않아서 standalone 서버에만 적합한 세팅이다.
ubuntu
nginx
uwsgi
내가 맡은 서버는 거의 초 마이크로 스케일이라서 한 서버에 여러개의 서비스가 올라가 있는 경우가 많다. 이 앱들을 명확히 구분하기 위해 one user, one db, one service 형태로 진행된다.
이 글에서 쓰일 예제명은 plasmid
우선 root로 서버에 접속해서
$ adduser plasmid $ usermod -a -G admin plasmid
로 sudo 권한을 주고 plasmid 계정으로 재접속. 가장 먼저 할 일은 역시 virtualenv
$ cd ~ $ sudo pip install virtualenv $ virtualenv venv $ source ~/venv/bin/activate
이 계정은 이 프로젝트를 위해서 존재하기 때문에, 항상 virtualenv를 켜놔도 된다. ~/.profile (혹은 그 에 준하는 파일) 맨 아래에 스크립트를 추가
source /home/plasmid/venv/bin/activate
그 후 django, uwsgi 등등을 설치
(venv)$ pip install django uwsgi
사실 내가 사용하는 스크립트는 다음과 같다.
$ sudo apt-get upgrade $ sudo apt-get update $ sudo apt-get install build-essential git python-pip python-dev mysql-server libmysqlclient-dev php5-cli php5-fpm php5-mysql nginx phpmyadmin $ sudo easy_install -U distribute
(venv)$ pip install django uwsgi fabric south django-assets mysql-python pil wand bpython
php는 사실상 phpmyadmin 용. 저렇게 설치해두면 mysql 기준으로 내가 쓰는건 거의 다 설치된다.
그 다음은 프로젝트 생성과 디렉토리 구조 관리. 우선 ~ 아래에 프로젝트를 생성하고, 그 프로젝트는 사실상 전부 VCS에 포함시킨다. 즉, 컴파일된 static들이나 media, log 등등을 프로젝트와 분리하겠다는것.
(venv)$ cd /home/plasmid (venv)$ django-admin.py startproject plasmid (venv)$ mkdir static media log misc (venv)$ cd log (venv)$ touch access.log error.log plasmid.log
여기서 ~/media를 외부 CDN에 NFS로 묶거나 아예 쓰지 않고 S3 같은걸로 관리를 하는 식으로 사용한다.
~/static에는 css, js, images, fonts 정도의 폴더를 생성해두고, 각 폴더엔 최종본만 배치한다. 이렇게 함으로써 nginx같은 웹서버 혹은 그 누구도 django 프로젝트에 접근 할 일을 없앨 수 있다. 이에 대해선 managing static files를 정독. 난 webassets을 사용하기때문에 최종 결과물인 dist.js 같은 아이들만 넣어둔다. 첨언을 하자면 나의 경우 js, css에 대해 라이브러리를 전부 묶어서 lib.js, lib.css로 두고, coffee와 less를 써서 코딩을 한 후 compile - merge - uglify 과정을 거쳐서 dist.js, dist.css의 형태로 배포한다. 이걸 webassets이 한방에 해준다.
이제 프로젝트 내부에서 VCS나 DB, app, url dispatcher, view 등등 처리를 해서 runserver를 했을때 잘 작동되는 상태까지 만들자. 이 부분에 대해선 여기로 넘기겠음.
그 다음은 nginx 세팅. 분산 서버니 클라우드니 SSL이니 달다보면 점점 복잡해지지만 기본 형태는 간단하다.
(venv)$ sudo vim /etc/nginx/sites-enabled/plasmid.joyfl.net
server { listen 80; server_name plasmid.joyfl.net; access_log /home/plasmid/log/access.log; error_log /home/plasmid/log/error.log; location / { include /etc/nginx/uwsgi_params; uwsgi_pass unix:/home/plasmid/misc/plasmid.sock; } location /static { alias /home/plasmid/static; } location /media { alias /home/plasmid/media; } }
(venv)$ sudo nginx -t (venv)$ sudo service nginx reload
위에서 /home/plasmid/misc라는 폴더를 만들었는데, 보통 .sock이나 .pid 파일들은 /tmp에 보관하기도 하지만 난 그냥 모든걸 ~에 두고 싶어서 이렇게 했다. 이 두가지 파일들은 이제 아래에 나올, uwsgi를 가동시킬때 적절히 생겨서, 결론적으로 nginx와 uwsgi가 연결되고, uwsgi는 django를 가동시키게 된다.
static과 media에 대해선 사실 하기 나름이다. subdomain까지 분리해서, 혹은 서버를 달리해서 서빙 할 수도 있고, 이렇게 url 만 잡아서 alias나 root명령어로 넘기거��, 혹은 저 부분을 다 지워버리고 django의 static view를 써서 django가 다 처리하게끔 해도 된다. 다만 문서에도 써있듯이 좋은 방법은 아니다. 로드도 많이 생기고 웹서버가 해도 될 일이고 보안 이슈가 생길수도 있고 등등. 아래에 보면 debugging 전용 코드 만들기를 예제로 쓸 정도니.
여튼 이제 nginx는 들어온 요청을 저 plasmid.sock으로 전달한다. 그러니 이제 uwsgi로 django를 가동시켜두면 된다.
그 과정은 간단하게, .ini 파일을 만들어서 세팅을 해두고, uwsgi 명령어로 실행시키는 느낌이다. 일단 프로젝트 폴더에 uwsgi.ini 파일을 만들자.
(venv)$ cd /home/plasmid/plasmid (venv)$ vim uwsgi.ini
[uwsgi] uid = plasmid socket = /home/plasmid/misc/plasmid.sock pidfile = /home/plasmid/misc/plasmid.pid pythonpath = /home/plasmid/venv/bin pythonhome = /home/plasmid/venv/bin daemonize = /home/plasmid/log/plasmid.log home = /home/plasmid/venv env = DJANGO_SETTINGS_MODULE=plasmid.settings module = django.core.handlers.wsgi:WSGIHandler() master = true processes = 4 harakiri = 60 reload-mercy = 8 max-requests = 4000 limit-as = 512 reload-on-as = 256 reload-on-rss = 192 no-orphans = true vacuum = true log-date = true
세팅 끝. run, reload, kill은 각각
(venv)$ uwsgi uwsgi.ini --enable-threads
(venv)$ uwsgi --reload /home/plasmid/misc/plasmid.pid
(venv)$ sudo kill -9 $(cat /home/plasmid/misc/plasmid.pid)
이다. fabric으로 만들어두거나 간단하게 bash, perl 등으로 스크립트를 짜두면 된다. 물론 fabric을 쓰겠지만..
이 .ini에 대해선 좀 할 이야기가 많은데, 사실 .sock 파일 경로 같은걸 지정 하다보니 VCS에 포함시키긴 좀 애매하다. 그래서 여긴 사실 좀 하기 나름이다. standalone 서버라면 위 구조대로 해도 상관 없는데 (로컬 개발자들은 .gitignore 같은거 쓰라고 시키고) 클라우드 환경이라면 jinja로 렌더링 하는 사람도 봤지만 결국엔 fabric, ENV 등을 활용하여 하나의 코드를 쓰되 각 서버별 스스로의 상황에 맞게끔 해줘야한다. 이에 대해선 내가 자세히 다룰 수 있는 주제는 아니고 (경험 부족 + 너무 하기 나름이라..) 이것, 저것, 요것, 고것을 정독하고 마법의 fabfile.py를 만들어내면 된다.
결론적으로, 나의 경우 아래와 같은 디렉토리 구조를 갖게 된다.
/home/plasmid ($HOME) ├── log │ ├── access.log │ ├── error.log │ └── plasmid.log ├── media ├── misc │ ├── plasmid.pid │ └── plasmid.sock ├── plasmid ($BASE_DIR) │ ├── uwsgi.ini │ ├── plasmid │ ├── manage.py │ ... └── static ├── css │ ├── dist.css │ └── lib.css ├── fonts ├── images └── js ├── dist.js └── lib.js
프로세스를 확인해보면
(venv)$ ps -ef | grep uwsgi
1234 9000 1 0 Nov22 ? 00:00:17 uwsgi uwsgi.ini --enable-threads 1234 9001 9000 0 Nov22 ? 00:00:00 uwsgi uwsgi.ini --enable-threads 1234 9002 9000 0 Nov22 ? 00:00:00 uwsgi uwsgi.ini --enable-threads 1234 9003 9000 0 Nov22 ? 00:00:00 uwsgi uwsgi.ini --enable-threads 1234 9004 9000 0 Nov22 ? 00:00:00 uwsgi uwsgi.ini --enable-threads
uwsgi가 정상적으로 django를 실행시켜서 프로세스를 띄우고 있고, 거기에 nginx가 잘 넘겨 줄것이다. url 을 치고 들어가면 "Hello, world!" 가 떠있을것이다.
결론은, 역시 "하기 나름". 이 글은 그저 standalone 서버를 쓰는 사람의 예제다.
2 notes
·
View notes
Link
친구들한테 수학 문제를 물어보거나 페북같은 SNS 위에서 수식을 공유하고 싶을때 쓰기 위해 만듦.
블로그 같은곳에 쓰는 글은 보통 LaTeX 플러그인을 쓰기 때문에 code처럼 이쁘게 쓸 수 있지만 페북/트위터계열이나 메신저에선 링크로 전달하는게 편할 수도 있지 않을까.
실제로 간단한 코드를 보낼땐 gist를 써서 링크만 보내는 경우가 많음.
사용법은, 설명은 글로, 수식은 $로 래핑해서 사용.
Let $x \in \mathbb{R}$ be $x^2 = 0$. Prove $x = 0$.
위와 같이 쓰면
Let \(x \in \mathbb{R}\) be \(x^2 = 0\). Prove \(x = 0\). 로 렌더링 해줌.
사용하는 라이브러리는 mathjax, 백엔드는 역시나 django
0 notes